diff --git a/build.gradle b/build.gradle index 645a83f01..e2e9fddb6 100755 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { - id 'com.android.application' version '7.1.2' apply false - id 'com.android.library' version '7.1.2' apply false - id 'org.jetbrains.kotlin.android' version '1.6.10' apply false - id 'com.google.dagger.hilt.android' version '2.41' apply false + id 'com.android.application' version '8.0.2' apply false + id 'com.android.library' version '8.0.2' apply false + id 'org.jetbrains.kotlin.android' version '1.8.10' apply false + id 'com.google.dagger.hilt.android' version '2.46.1' apply false } task clean(type: Delete) { diff --git a/gradle.properties b/gradle.properties index 1b0fafdba..743c92217 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,7 @@ android.enableJetifier=true org.gradle.jvmargs=-Xmx2048m # Application properties -VERSION_CODE=1 \ No newline at end of file +VERSION_CODE=1 +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bbbd07c83..928485258 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu May 06 13:38:17 MDT 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/mage/build.gradle b/mage/build.gradle index 0c619ca55..036cbb066 100644 --- a/mage/build.gradle +++ b/mage/build.gradle @@ -5,13 +5,12 @@ plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-parcelize' id 'kotlin-kapt' - id 'maven-publish' id 'org.ajoberstar.grgit' version '4.1.0' id 'com.google.dagger.hilt.android' } group 'mil.nga.giat.mage' -version '7.2.2' +version '7.2.1' ext { sourceRefspec = Grgit.open(currentDir: project.rootDir).head().id @@ -34,7 +33,7 @@ def googleMapsApiReleaseKey = hasProperty('RELEASE_MAPS_API_KEY') ? RELEASE_MAPS def googleMapsApiDebugKey = hasProperty('DEBUG_MAPS_API_KEY') ? DEBUG_MAPS_API_KEY : '' android { - compileSdkVersion 33 + compileSdkVersion 34 flavorDimensions "default" dataBinding { @@ -47,13 +46,18 @@ android { } compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "1.8" - freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"] + kotlin { + jvmToolchain(11) + } + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) { + kotlinOptions { + jvmTarget = "11" + } } buildFeatures { @@ -62,15 +66,15 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.1.1" + kotlinCompilerExtensionVersion = "1.4.3" } defaultConfig { applicationId "mil.nga.giat.mage" versionName project.version versionCode VERSION_CODE as int - minSdkVersion 25 - targetSdkVersion 32 + minSdkVersion 27 + targetSdkVersion 33 multiDexEnabled true resValue "string", "serverURLDefaultValue", serverURL resValue "string", "recentMapXYZDefaultValue", "263.0,40.0,3" @@ -117,15 +121,6 @@ android { } } - productFlavors { - defaults { - // if you build without a flavor gradle will build all flavors - // define this flavor to provide a flavor we can build with no overrides - } - wear { - resValue "bool", "isWearBuildDefaultValue", "true" - } - } packagingOptions { resources { excludes += ['META-INF/LICENSE', 'META-INF/LICENSE.txt', 'META-INF/NOTICE', 'META-INF/NOTICE.txt', 'META-INF/INDEX.LIST'] @@ -142,79 +137,71 @@ android { useLibrary 'android.test.runner' useLibrary 'android.test.base' useLibrary 'android.test.mock' + namespace 'mil.nga.giat.mage' } dependencies { ext { - lifecycle_version= '2.4.1' - room_version = '2.4.2' - } - - // this block allows the mage-android-wear-bridge to be pulled in dynamically when that flavor is built - gradle.startParameter.taskRequests.each { taskRequest -> - taskRequest.args.each { taskName -> - if (taskName.toLowerCase().contains("wear")) { - //wearCompile project(':bridge') // uncomment me to build wearable locally - wearCompile "mil.nga.giat.mage.wearable:bridge:1.0.0" // uncomment me to build wearable. comment me if building wearable locally. - } - } + lifecycle_version= '2.6.2' + room_version = '2.5.2' } - implementation "androidx.core:core-ktx:1.7.0" - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "androidx.core:core-ktx:1.12.0" + implementation "androidx.fragment:fragment-ktx:1.6.1" + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0' - implementation 'androidx.mediarouter:mediarouter:1.2.6' - implementation 'androidx.recyclerview:recyclerview:1.2.1' - implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'androidx.mediarouter:mediarouter:1.6.0' + implementation 'androidx.recyclerview:recyclerview:1.3.1' + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.browser:browser:1.4.0' - implementation 'androidx.preference:preference-ktx:1.2.0' - implementation 'androidx.preference:preference-ktx:1.2.0' - implementation "com.google.android.gms:play-services-maps:18.0.2" - implementation 'com.google.maps.android:maps-ktx:3.2.1' + implementation 'androidx.browser:browser:1.6.0' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.preference:preference-ktx:1.2.1' + implementation "com.google.android.gms:play-services-maps:18.1.0" + implementation 'com.google.maps.android:maps-ktx:3.4.0' - implementation ('androidx.browser:browser:1.4.0') + implementation ('androidx.browser:browser:1.6.0') - implementation 'com.google.dagger:hilt-android:2.41' - kapt 'com.google.dagger:hilt-compiler:2.41' + implementation 'com.google.dagger:hilt-android:2.46.1' + kapt 'com.google.dagger:hilt-compiler:2.46.1' implementation 'androidx.hilt:hilt-work:1.0.0' kapt 'androidx.hilt:hilt-compiler:1.0.0' annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0' - implementation 'com.google.android.material:material:1.5.0' + implementation 'com.google.android.material:material:1.9.0' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" - implementation 'androidx.compose.ui:ui:1.1.1' - implementation 'androidx.compose.ui:ui-tooling:1.1.1' - implementation 'androidx.compose.foundation:foundation:1.1.1' - implementation 'androidx.compose.runtime:runtime-livedata:1.1.1' - implementation 'androidx.compose.runtime:runtime-rxjava2:1.1.1' - implementation 'androidx.compose.material:material:1.1.1' - implementation 'androidx.compose.material:material-icons-core:1.1.1' - implementation 'androidx.compose.material:material-icons-extended:1.1.1' - implementation 'androidx.activity:activity-compose:1.4.0' + implementation 'androidx.compose.ui:ui:1.5.1' + implementation 'androidx.compose.ui:ui-tooling:1.5.1' + implementation 'androidx.compose.foundation:foundation:1.5.1' + implementation 'androidx.compose.runtime:runtime-livedata:1.5.1' + implementation 'androidx.compose.runtime:runtime-rxjava2:1.5.1' + implementation 'androidx.compose.material:material:1.5.1' + implementation 'androidx.compose.material:material-icons-core:1.5.1' + implementation 'androidx.compose.material:material-icons-extended:1.5.1' + implementation 'androidx.activity:activity-compose:1.7.2' implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "com.google.accompanist:accompanist-swiperefresh:0.19.0" - implementation('androidx.paging:paging-runtime-ktx:3.1.1') - implementation("androidx.paging:paging-compose:1.0.0-alpha14") + implementation('androidx.paging:paging-runtime-ktx:3.2.1') + implementation("androidx.paging:paging-compose:3.2.1") - implementation ('androidx.work:work-runtime-ktx:2.7.1') + implementation ('androidx.work:work-runtime-ktx:2.8.1') implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-jackson:2.9.0' - implementation 'com.squareup.okhttp3:okhttp:3.14.9' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:okhttp-urlconnection:3.11.0' implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-paging:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" - implementation 'com.j256.ormlite:ormlite-android:5.6' + implementation 'com.j256.ormlite:ormlite-android:6.1' implementation 'com.google.maps.android:android-maps-utils:2.3.0' implementation 'mil.nga.geopackage.map:geopackage-android-map:6.7.1' @@ -230,11 +217,10 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.2.3' - implementation 'androidx.exifinterface:exifinterface:1.3.3' - implementation "com.google.android.exoplayer:exoplayer:2.17.1" + implementation 'androidx.exifinterface:exifinterface:1.3.6' + implementation "com.google.android.exoplayer:exoplayer:2.19.1" - implementation 'com.google.code.gson:gson:2.8.7' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0' + implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.guava:guava:31.0-android' implementation 'org.apache.commons:commons-lang3:3.3.2' diff --git a/mage/schemas/mil.nga.giat.mage.database.MageDatabase/1.json b/mage/schemas/mil.nga.giat.mage.database.MageDatabase/1.json new file mode 100644 index 000000000..eaec8314f --- /dev/null +++ b/mage/schemas/mil.nga.giat.mage.database.MageDatabase/1.json @@ -0,0 +1,237 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "19675c2152405204cb6e30aecf9af7e3", + "entities": [ + { + "tableName": "feed", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `summary` TEXT, `items_have_identity` INTEGER NOT NULL, `items_have_spatial_dimension` INTEGER NOT NULL, `event_remote_id` TEXT NOT NULL, `update_frequency` INTEGER, `item_temporal_property` TEXT, `item_primary_property` TEXT, `item_secondary_property` TEXT, `item_properties_schema` TEXT, `constant_params` TEXT, `variable_params` TEXT, `map_style_stroke` TEXT, `map_style_stroke_opacity` REAL, `map_style_stroke_width` INTEGER, `map_style_fill` TEXT, `map_style_fill_opacity` REAL, `map_style_icon_style_id` TEXT, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "itemsHaveIdentity", + "columnName": "items_have_identity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "itemsHaveSpatialDimension", + "columnName": "items_have_spatial_dimension", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eventRemoteId", + "columnName": "event_remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updateFrequency", + "columnName": "update_frequency", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "itemTemporalProperty", + "columnName": "item_temporal_property", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "itemPrimaryProperty", + "columnName": "item_primary_property", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "itemSecondaryProperty", + "columnName": "item_secondary_property", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "itemPropertiesSchema", + "columnName": "item_properties_schema", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "constantParams", + "columnName": "constant_params", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "variableParams", + "columnName": "variable_params", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mapStyle.stroke", + "columnName": "map_style_stroke", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mapStyle.strokeOpacity", + "columnName": "map_style_stroke_opacity", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "mapStyle.strokeWidth", + "columnName": "map_style_stroke_width", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mapStyle.fill", + "columnName": "map_style_fill", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mapStyle.fillOpacity", + "columnName": "map_style_fill_opacity", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "mapStyle.iconStyle.id", + "columnName": "map_style_icon_style_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feed_local", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`feed_id` TEXT NOT NULL, `last_sync` INTEGER, PRIMARY KEY(`feed_id`), FOREIGN KEY(`feed_id`) REFERENCES `feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "feedId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "feed_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "feed", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "feed_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "feed_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `geometry` BLOB, `properties` TEXT, `feed_id` TEXT NOT NULL, `timestamp` INTEGER, PRIMARY KEY(`id`, `feed_id`), FOREIGN KEY(`feed_id`) REFERENCES `feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "geometry", + "columnName": "geometry", + "affinity": "BLOB", + "notNull": false + }, + { + "fieldPath": "properties", + "columnName": "properties", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "feedId", + "columnName": "feed_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "feed_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "feed", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "feed_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '19675c2152405204cb6e30aecf9af7e3')" + ] + } +} \ No newline at end of file diff --git a/mage/src/main/AndroidManifest.xml b/mage/src/main/AndroidManifest.xml index 46d176d5a..4dc51dd93 100644 --- a/mage/src/main/AndroidManifest.xml +++ b/mage/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> - @@ -17,14 +15,12 @@ - - + android:required="true" /> @@ -32,14 +28,16 @@ + android:networkSecurityConfig="@xml/network_security_config" + android:allowBackup="false" + android:fullBackupContent="false" + android:dataExtractionRules="@xml/data_extraction_rules" tools:targetApi="s"> @@ -69,7 +67,6 @@ @@ -128,8 +125,10 @@ - - + + + + + android:theme="@style/AppTheme.NoActionBar" /> + android:name=".filter.FilterActivity" /> @@ -238,14 +235,6 @@ - - - - - - bottomNavigationFragments = new ArrayList<>(); - - private String openPath; - - private ActivityLandingBinding binding; - - ActivityResultLauncher feedIntentLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> { - if (result.getResultCode() == Activity.RESULT_OK) { - Intent data = result.getData(); - if (data != null) { - FeedActivity.ResultType resultType = (FeedActivity.ResultType) data.getSerializableExtra(FeedActivity.FEED_ITEM_RESULT_TYPE); - onFeedResult(resultType, data); - } - } - } - ); - - ActivityResultLauncher reportLocationIntent = registerForActivityResult( - new LocationPermission(), result -> { - if (result.getPreciseGranted() || result.getCoarseGranted()) { - application.startLocationService(); - } else { - application.stopLocationService(); - } - } - ); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - SharedPreferences googleBug = getSharedPreferences("google_bug_154855417", Context.MODE_PRIVATE); - if (!googleBug.contains("fixed")) { - File corruptedZoomTables = new File(getFilesDir(), "ZoomTables.data"); - corruptedZoomTables.delete(); - googleBug.edit().putBoolean("fixed", true).apply(); - } - - binding = ActivityLandingBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - binding.navigation.setNavigationItemSelectedListener(this); - - currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - - bottomNavigationFragments.add(new MapFragment()); - bottomNavigationFragments.add(new ObservationFeedFragment()); - bottomNavigationFragments.add(new UserFeedFragment()); - - // TODO investigate moving this call - // its here because this is the first activity started after login and it ensures - // the user has selected an event. However there are other instances that could - // bring the user back to this activity in which this has already been called, - // i.e. after TokenExpiredActivity. - application.onLogin(); - - CacheProvider.getInstance(getApplicationContext()).refreshTileOverlays(); - - Event event = EventHelper.getInstance(getApplicationContext()).getCurrentEvent(); - onTitle(event); - setRecentEvents(event); - - setSupportActionBar(binding.toolbar); - - reportLocationIntent.launch(null); - - binding.toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp); - binding.toolbar.setNavigationOnClickListener(v -> binding.drawerLayout.openDrawer(GravityCompat.START)); - - View headerView = binding.navigation.getHeaderView(0); - headerView.setOnClickListener(v -> onNavigationItemSelected(binding.navigation.getMenu().findItem(R.id.profile_navigation))); - - // Check if MAGE was launched with a local file - openPath = getIntent().getStringExtra(EXTRA_OPEN_FILE_PATH); - if (openPath != null) { - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle) - .setTitle(R.string.cache_access_rationale_title) - .setMessage(R.string.cache_access_rationale_message) - .setPositiveButton(android.R.string.ok, (dialog, which) -> ActivityCompat.requestPermissions(LandingActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_OPEN_FILE)) - .show(); - } else { - ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_OPEN_FILE); - } - } else { - // Else, store the path to pass to further intents - handleOpenFilePath(); - } - } - - binding.bottomNavigation.setOnItemSelectedListener(item -> { - switch (item.getItemId()) { - case R.id.map_tab: - viewModel.setNavigationTab(LandingViewModel.NavigationTab.MAP); - break; - case R.id.observations_tab: - viewModel.setNavigationTab(LandingViewModel.NavigationTab.OBSERVATIONS); - break; - case R.id.people_tab: - viewModel.setNavigationTab(LandingViewModel.NavigationTab.PEOPLE); - break; - } - - return true; - }); - - viewModel = new ViewModelProvider(this).get(LandingViewModel.class); - viewModel.getFilterText().observe(this, this::setSubtitle); - viewModel.getNavigationTab().observe(this, this::onNavigationTab); - viewModel.getFeeds().observe(this, this::setFeeds); - viewModel.setEvent(event.getRemoteId()); - } - - @Override - protected void onResume() { - super.onResume(); - - MenuItem selectedItem = binding.bottomNavigation.getMenu().findItem(binding.bottomNavigation.getSelectedItemId()); - switch (selectedItem.getItemId()) { - case R.id.map_tab: { - viewModel.setNavigationTab(LandingViewModel.NavigationTab.MAP); - break; - } - case R.id.observations_tab: { - viewModel.setNavigationTab(LandingViewModel.NavigationTab.OBSERVATIONS); - break; - } - case R.id.people_tab: { - viewModel.setNavigationTab(LandingViewModel.NavigationTab.PEOPLE); - break; - } - } - - View headerView = binding.navigation.getHeaderView(0); - try { - final ImageView avatarImageView = headerView.findViewById(R.id.avatar_image_view); - User user = UserHelper.getInstance(getApplicationContext()).readCurrentUser(); - GlideApp.with(this) - .load(Avatar.Companion.forUser(user)) - .circleCrop() - .fallback(R.drawable.ic_account_circle_white_48dp) - .error(R.drawable.ic_account_circle_white_48dp) - .into(avatarImageView); - - TextView displayName = headerView.findViewById(R.id.display_name); - displayName.setText(user.getDisplayName()); - - TextView email = headerView.findViewById(R.id.email); - email.setText(user.getEmail()); - email.setVisibility(StringUtils.isNoneBlank(user.getEmail()) ? View.VISIBLE : View.GONE); - } catch (UserException e) { - Log.e(LOG_NAME, "Error pulling current user from the database", e); - } - - // This activity is 'singleTop' and as such will not recreate itself based on a uiMode configuration change. - // Force this by check if the uiMode has changed. - int nightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; - if (nightMode != currentNightMode) { - recreate(); - } - - if (shouldReportLocation()) { - if (locationAccess.isLocationGranted()) { - application.startLocationService(); - } - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - switch (requestCode) { - case PERMISSIONS_REQUEST_ACCESS_STORAGE: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - CacheProvider.getInstance(getApplicationContext()).refreshTileOverlays(); - } - - break; - } - case PERMISSIONS_REQUEST_OPEN_FILE: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - handleOpenFilePath(); - } else { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - // User denied storage with never ask again. Since they will get here - // by opening a cache into MAGE, give them a dialog that will - // by opening a cache into MAGE, give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - getResources().getString(R.string.cache_access_title), - getResources().getString(R.string.cache_access_message)); - } - } - - break; - } - } - } - - private void setFeeds(List feeds) { - Menu menu = binding.navigation.getMenu(); - Menu feedsMenu = menu.findItem(R.id.feeds_item).getSubMenu(); - feedsMenu.removeGroup(R.id.feeds_group); - - int i = 1; - for (final Feed feed : feeds) { - - MenuItem item = feedsMenu - .add(R.id.feeds_group, Menu.NONE, i++, feed.getTitle()) - .setIcon(R.drawable.ic_rss_feed_24); - - // TODO get feed icon when available -// if (feed.getMapStyle().getIconUrl() != null) { -// int px = (int) Math.floor(TypedValue.applyDimension( -// TypedValue.COMPLEX_UNIT_DIP, -// 24f, -// getResources().getDisplayMetrics())); -// -// Glide.with(this) -// .asBitmap() -// .load(feed.getMapStyle().getIconUrl()) -// .transform(new MultiTransformation<>(new FitCenter(), new PadToFrame())) -// .into(new CustomTarget(px, px) { -// @Override -// public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { -// item.setIcon(new BitmapDrawable(getResources(), resource)); -// } -// -// @Override -// public void onLoadCleared(@Nullable Drawable placeholder) {} -// }); -// } - - item.setOnMenuItemClickListener(menuItem -> { - binding.drawerLayout.closeDrawer(GravityCompat.START); - Intent intent = FeedActivity.Companion.intent(LandingActivity.this, feed); - feedIntentLauncher.launch(intent); - return true; - }); - } - } - - private void setRecentEvents(Event event) { - Menu menu = binding.navigation.getMenu(); - - Menu recentEventsMenu = menu.findItem(R.id.recents_events_item).getSubMenu(); - recentEventsMenu.removeGroup(R.id.events_group); - - EventHelper eventHelper = EventHelper.getInstance(getApplicationContext()); - try { - menu.findItem(R.id.event_navigation).setTitle(event.getName()).setActionView(R.layout.navigation_item_info); - - Iterable recentEvents = Iterables.filter(eventHelper.getRecentEvents(), recentEvent -> !recentEvent.getRemoteId().equals(event.getRemoteId())); - - int i = 1; - for (final Event recentEvent : recentEvents) { - MenuItem item = recentEventsMenu - .add(R.id.events_group, Menu.NONE, i++, recentEvent.getName()) - .setIcon(R.drawable.ic_restore_black_24dp); - - item.setOnMenuItemClickListener(menuItem -> { - binding.drawerLayout.closeDrawer(GravityCompat.START); - Intent intent = new Intent(LandingActivity.this, EventsActivity.class); - intent.putExtra(EventsActivity.Companion.getEVENT_ID_EXTRA(), recentEvent.getId()); - startActivityForResult(intent, CHANGE_EVENT_REQUEST); - return true; - }); - } - - MenuItem item = recentEventsMenu - .add(R.id.events_group, Menu.NONE, i, "More Events") - .setIcon(R.drawable.ic_event_note_white_24dp); - - item.setOnMenuItemClickListener(menuItem -> { - binding.drawerLayout.closeDrawer(GravityCompat.START); - Intent intent = new Intent(LandingActivity.this, EventsActivity.class); - intent.putExtra(EventsActivity.Companion.getCLOSABLE_EXTRA(), true); - startActivityForResult(intent, CHANGE_EVENT_REQUEST); - return true; - }); - } catch (EventException e) { - e.printStackTrace(); - } - } - - private void showDisabledPermissionsDialog(String title, String message) { - new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.settings, (dialog, which) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", getApplicationContext().getPackageName(), null)); - startActivity(intent); - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - @Override - public boolean onNavigationItemSelected(MenuItem menuItem) { - binding.drawerLayout.closeDrawer(GravityCompat.START); - - switch (menuItem.getItemId()) { - case R.id.event_navigation: { - Event event = EventHelper.getInstance(getApplicationContext()).getCurrentEvent(); - Intent intent = new Intent(LandingActivity.this, EventActivity.class); - intent.putExtra(EventActivity.EVENT_ID_EXTRA, event.getId()); - startActivityForResult(intent, CHANGE_EVENT_REQUEST); - break; - } - case R.id.profile_navigation: { - Intent intent = new Intent(this, ProfileActivity.class); - startActivity(intent); - break; - } - case R.id.settings_navigation: { - Intent intent = new Intent(this, GeneralPreferencesActivity.class); - startActivity(intent); - break; - } - case R.id.help_navigation: { - Intent intent = new Intent(this, HelpActivity.class); - startActivity(intent); - break; - } - case R.id.logout_navigation: { - application.onLogout(true, () -> { - Intent intent = new Intent(getApplicationContext(), LoginActivity.class); - startActivity(intent); - finish(); - }); - break; - } - } - - return false; - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - - if (requestCode == CHANGE_EVENT_REQUEST) { - if (resultCode == RESULT_OK) { - Event event = EventHelper.getInstance(getApplicationContext()).getCurrentEvent(); - onTitle(event); - setRecentEvents(event); - viewModel.setEvent(event.getRemoteId()); - } - } - } - - private void onFeedResult(FeedActivity.ResultType resultType, Intent data) { - if (resultType == FeedActivity.ResultType.NAVIGATE) { - String feedId = data.getStringExtra(FeedActivity.FEED_ID_EXTRA); - String itemId = data.getStringExtra(FeedActivity.FEED_ITEM_ID_EXTRA); - viewModel.startFeedNavigation(feedId, itemId); - } - } - - private void onTitle(Event event) { - binding.toolbar.setTitle(event.getName()); - } - - private void setSubtitle(String subtitle) { - binding.toolbar.setSubtitle(subtitle); - } - - private void onNavigationTab(LandingViewModel.NavigationTab tab) { - Fragment fragment = null; - switch (tab) { - case MAP: - binding.bottomNavigation.getMenu().getItem(0).setChecked(true); - fragment = bottomNavigationFragments.get(0); - break; - case OBSERVATIONS: - binding.bottomNavigation.getMenu().getItem(1).setChecked(true); - fragment = bottomNavigationFragments.get(1); - break; - case PEOPLE: - binding.bottomNavigation.getMenu().getItem(2).setChecked(true); - fragment = bottomNavigationFragments.get(2); - break; - } - - if (fragment != null) { - FragmentManager fragmentManager = getSupportFragmentManager(); - fragmentManager.beginTransaction().replace(R.id.navigation_content, fragment).commit(); - } - } - - private boolean shouldReportLocation() { - boolean reportLocation = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(getString(R.string.reportLocationKey), getResources().getBoolean(R.bool.reportLocationDefaultValue)); - boolean inEvent = UserHelper.getInstance(getApplicationContext()).isCurrentUserPartOfCurrentEvent(); - - return reportLocation && inEvent; - } - - /** - * Handle opening the file path that MAGE was launched with - */ - private void handleOpenFilePath() { - - File cacheFile = new File(openPath); - - // Handle GeoPackage files by linking them to their current location - if (GeoPackageValidate.hasGeoPackageExtension(cacheFile)) { - - String cacheName = GeoPackageCacheUtils.importGeoPackage(this, cacheFile); - if (cacheName != null) { - CacheProvider.getInstance(getApplicationContext()).enableAndRefreshTileOverlays(cacheName); - } - } - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/LandingActivity.kt b/mage/src/main/java/mil/nga/giat/mage/LandingActivity.kt new file mode 100644 index 000000000..a4a2f230d --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/LandingActivity.kt @@ -0,0 +1,415 @@ +package mil.nga.giat.mage + +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.GravityCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.preference.PreferenceManager +import com.google.android.material.navigation.NavigationView +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.geopackage.validate.GeoPackageValidate +import mil.nga.giat.mage.LandingViewModel.NavigationTab +import mil.nga.giat.mage.cache.GeoPackageCacheUtils +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.databinding.ActivityLandingBinding +import mil.nga.giat.mage.event.EventActivity +import mil.nga.giat.mage.event.EventsActivity +import mil.nga.giat.mage.event.EventsActivity.Companion.CLOSABLE_EXTRA +import mil.nga.giat.mage.event.EventsActivity.Companion.EVENT_ID_EXTRA +import mil.nga.giat.mage.feed.FeedActivity +import mil.nga.giat.mage.feed.FeedActivity.Companion.intent +import mil.nga.giat.mage.glide.GlideApp +import mil.nga.giat.mage.glide.model.Avatar.Companion.forUser +import mil.nga.giat.mage.help.HelpActivity +import mil.nga.giat.mage.location.LocationAccess +import mil.nga.giat.mage.location.LocationContractResult +import mil.nga.giat.mage.location.LocationPermission +import mil.nga.giat.mage.login.LoginActivity +import mil.nga.giat.mage.map.MapFragment +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.newsfeed.ObservationFeedFragment +import mil.nga.giat.mage.newsfeed.UserFeedFragment +import mil.nga.giat.mage.preferences.GeneralPreferencesActivity +import mil.nga.giat.mage.profile.ProfileActivity +import org.apache.commons.lang3.StringUtils +import java.io.File +import javax.inject.Inject + +/** + * This is the Activity that holds other fragments. Map, feeds, etc. It + * starts and stops much of the context. It also contains menus . + */ +@AndroidEntryPoint +class LandingActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener { + @Inject lateinit var application: MageApplication + @Inject lateinit var locationAccess: LocationAccess + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + @Inject lateinit var cacheProvider: CacheProvider + + private lateinit var viewModel: LandingViewModel + private var currentNightMode = 0 + private val bottomNavigationFragments: MutableList = ArrayList() + private var binding: ActivityLandingBinding? = null + + private var feedIntentLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == RESULT_OK) { + val data = result.data + if (data != null) { + val resultType = data.getSerializableExtra(FeedActivity.FEED_ITEM_RESULT_TYPE) as FeedActivity.ResultType? + onFeedResult(resultType, data) + } + } + } + + private var reportLocationIntent: ActivityResultLauncher<*> = registerForActivityResult( + LocationPermission() + ) { (coarseGranted, preciseGranted): LocationContractResult -> + if (preciseGranted || coarseGranted) { + application.startLocationService() + } else { + application.stopLocationService() + } + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val googleBug = getSharedPreferences("google_bug_154855417", MODE_PRIVATE) + if (!googleBug.contains("fixed")) { + val corruptedZoomTables = File(filesDir, "ZoomTables.data") + corruptedZoomTables.delete() + googleBug.edit().putBoolean("fixed", true).apply() + } + binding = ActivityLandingBinding.inflate(layoutInflater) + setContentView(binding!!.root) + binding!!.navigation.setNavigationItemSelectedListener(this) + currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + bottomNavigationFragments.add(MapFragment()) + bottomNavigationFragments.add(ObservationFeedFragment()) + bottomNavigationFragments.add(UserFeedFragment()) + + // TODO investigate moving this call + // its here because this is the first activity started after login and it ensures + // the user has selected an event. However there are other instances that could + // bring the user back to this activity in which this has already been called, + // i.e. after TokenExpiredActivity. + application.onLogin() + val event = eventLocalDataSource.currentEvent + if (event != null) { + onTitle(event) + setRecentEvents(event) + // cacheProvider.refreshTileOverlays(); + } + setSupportActionBar(binding!!.toolbar) + reportLocationIntent.launch(null) + binding!!.toolbar.setNavigationIcon(R.drawable.ic_menu_white_24dp) + binding!!.toolbar.setNavigationOnClickListener { + binding!!.drawerLayout.openDrawer( + GravityCompat.START + ) + } + val headerView = binding!!.navigation.getHeaderView(0) + headerView.setOnClickListener { + onNavigationItemSelected( + binding!!.navigation.menu.findItem(R.id.profile_navigation) + ) + } + + // Check if MAGE was launched with a local file + val openPath = intent.getStringExtra(EXTRA_OPEN_FILE_PATH) + openPath?.let { handleOpenFilePath(it) } + binding!!.bottomNavigation.setOnItemSelectedListener { item: MenuItem -> + when (item.itemId) { + R.id.map_tab -> viewModel.setNavigationTab( + NavigationTab.MAP + ) + + R.id.observations_tab -> viewModel.setNavigationTab(NavigationTab.OBSERVATIONS) + R.id.people_tab -> viewModel.setNavigationTab(NavigationTab.PEOPLE) + } + true + } + viewModel = ViewModelProvider(this).get(LandingViewModel::class.java) + viewModel.filterText.observe(this) { subtitle: String -> setSubtitle(subtitle) } + viewModel.navigationTab.observe(this) { tab: NavigationTab -> onNavigationTab(tab) } + viewModel.feeds.observe(this) { feeds: List -> setFeeds(feeds) } + viewModel.setEvent(event!!.remoteId) + } + + override fun onResume() { + super.onResume() + val selectedItem = binding!!.bottomNavigation.menu.findItem( + binding!!.bottomNavigation.selectedItemId + ) + when (selectedItem.itemId) { + R.id.map_tab -> { + viewModel.setNavigationTab(NavigationTab.MAP) + } + + R.id.observations_tab -> { + viewModel.setNavigationTab(NavigationTab.OBSERVATIONS) + } + + R.id.people_tab -> { + viewModel.setNavigationTab(NavigationTab.PEOPLE) + } + } + val headerView = binding!!.navigation.getHeaderView(0) + val avatarImageView = headerView.findViewById(R.id.avatar_image_view) + val user = userLocalDataSource.readCurrentUser() + GlideApp.with(this) + .load(forUser(user!!)) + .circleCrop() + .fallback(R.drawable.ic_account_circle_white_48dp) + .error(R.drawable.ic_account_circle_white_48dp) + .into(avatarImageView) + val displayName = headerView.findViewById(R.id.display_name) + displayName.text = user.displayName + val email = headerView.findViewById(R.id.email) + email.text = user.email + email.visibility = if (StringUtils.isNoneBlank(user.email)) View.VISIBLE else View.GONE + + // This activity is 'singleTop' and as such will not recreate itself based on a uiMode configuration change. + // Force this by check if the uiMode has changed. + val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + if (nightMode != currentNightMode) { + recreate() + } + if (shouldReportLocation()) { + if (locationAccess.isLocationGranted()) { + application.startLocationService() + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when (requestCode) { + PERMISSIONS_REQUEST_ACCESS_STORAGE -> { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + CoroutineScope(Dispatchers.IO).launch { + cacheProvider.refreshTileOverlays() + } + } + } + } + } + + private fun setFeeds(feeds: List) { + val menu = binding!!.navigation.menu + val feedsMenu: Menu? = menu.findItem(R.id.feeds_item).subMenu + feedsMenu!!.removeGroup(R.id.feeds_group) + var i = 1 + for (feed in feeds) { + val item = feedsMenu + .add(R.id.feeds_group, Menu.NONE, i++, feed.title) + .setIcon(R.drawable.ic_rss_feed_24) + + // TODO get feed icon when available +// if (feed.getMapStyle().getIconUrl() != null) { +// int px = (int) Math.floor(TypedValue.applyDimension( +// TypedValue.COMPLEX_UNIT_DIP, +// 24f, +// getResources().getDisplayMetrics())); +// +// Glide.with(this) +// .asBitmap() +// .load(feed.getMapStyle().getIconUrl()) +// .transform(new MultiTransformation<>(new FitCenter(), new PadToFrame())) +// .into(new CustomTarget(px, px) { +// @Override +// public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { +// item.setIcon(new BitmapDrawable(getResources(), resource)); +// } +// +// @Override +// public void onLoadCleared(@Nullable Drawable placeholder) {} +// }); +// } + item.setOnMenuItemClickListener { menuItem: MenuItem? -> + binding!!.drawerLayout.closeDrawer(GravityCompat.START) + val intent = intent(this@LandingActivity, feed) + feedIntentLauncher.launch(intent) + true + } + } + } + + private fun setRecentEvents(event: Event?) { + val menu = binding!!.navigation.menu + val recentEventsMenu = menu.findItem(R.id.recents_events_item).subMenu + recentEventsMenu!!.removeGroup(R.id.events_group) + menu.findItem(R.id.event_navigation) + .setTitle(event!!.name) + .setActionView(R.layout.navigation_item_info) + + + val recentEvents = eventLocalDataSource.getRecentEvents().filterNot { it.remoteId == event.remoteId } + var i = 1 + for (recentEvent in recentEvents) { + val item = recentEventsMenu + .add(R.id.events_group, Menu.NONE, i++, recentEvent.name) + .setIcon(R.drawable.ic_restore_black_24dp) + item.setOnMenuItemClickListener { + binding!!.drawerLayout.closeDrawer(GravityCompat.START) + val intent = Intent(this@LandingActivity, EventsActivity::class.java) + intent.putExtra(EVENT_ID_EXTRA, recentEvent.id) + startActivityForResult(intent, CHANGE_EVENT_REQUEST) + true + } + } + val item = recentEventsMenu + .add(R.id.events_group, Menu.NONE, i, "More Events") + .setIcon(R.drawable.ic_event_note_white_24dp) + item.setOnMenuItemClickListener { + binding!!.drawerLayout.closeDrawer(GravityCompat.START) + val intent = Intent(this@LandingActivity, EventsActivity::class.java) + intent.putExtra(CLOSABLE_EXTRA, true) + startActivityForResult(intent, CHANGE_EVENT_REQUEST) + true + } + } + + override fun onNavigationItemSelected(menuItem: MenuItem): Boolean { + binding!!.drawerLayout.closeDrawer(GravityCompat.START) + when (menuItem.itemId) { + R.id.event_navigation -> { + val event = eventLocalDataSource.currentEvent + val intent = Intent(this@LandingActivity, EventActivity::class.java) + intent.putExtra(EventActivity.EVENT_ID_EXTRA, event!!.id) + startActivityForResult(intent, CHANGE_EVENT_REQUEST) + } + + R.id.profile_navigation -> { + val intent = Intent(this, ProfileActivity::class.java) + startActivity(intent) + } + + R.id.settings_navigation -> { + val intent = Intent(this, GeneralPreferencesActivity::class.java) + startActivity(intent) + } + + R.id.help_navigation -> { + val intent = Intent(this, HelpActivity::class.java) + startActivity(intent) + } + + R.id.logout_navigation -> { + application.onLogout(true) + val intent = Intent(applicationContext, LoginActivity::class.java) + startActivity(intent) + finish() + } + } + return false + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == CHANGE_EVENT_REQUEST) { + if (resultCode == RESULT_OK) { + val event = eventLocalDataSource.currentEvent + onTitle(event) + setRecentEvents(event) + viewModel.setEvent(event!!.remoteId) + } + } + } + + private fun onFeedResult(resultType: FeedActivity.ResultType?, data: Intent) { + if (resultType === FeedActivity.ResultType.NAVIGATE) { + val feedId = data.getStringExtra(FeedActivity.FEED_ID_EXTRA) + val itemId = data.getStringExtra(FeedActivity.FEED_ITEM_ID_EXTRA) + viewModel.startFeedNavigation(feedId!!, itemId!!) + } + } + + private fun onTitle(event: Event?) { + binding!!.toolbar.title = event!!.name + } + + private fun setSubtitle(subtitle: String) { + binding!!.toolbar.subtitle = subtitle + } + + private fun onNavigationTab(tab: NavigationTab) { + var fragment: Fragment? = null + when (tab) { + NavigationTab.MAP -> { + binding!!.bottomNavigation.menu.getItem(0).isChecked = true + fragment = bottomNavigationFragments[0] + } + + NavigationTab.OBSERVATIONS -> { + binding!!.bottomNavigation.menu.getItem(1).isChecked = true + fragment = bottomNavigationFragments[1] + } + + NavigationTab.PEOPLE -> { + binding!!.bottomNavigation.menu.getItem(2).isChecked = true + fragment = bottomNavigationFragments[2] + } + } + + val fragmentManager = supportFragmentManager + fragmentManager.beginTransaction().replace(R.id.navigation_content, fragment).commit() + } + + private fun shouldReportLocation(): Boolean { + val reportLocation = PreferenceManager.getDefaultSharedPreferences(this).getBoolean( + getString(R.string.reportLocationKey), + resources.getBoolean(R.bool.reportLocationDefaultValue) + ) + val inEvent = userLocalDataSource.isCurrentUserPartOfCurrentEvent() + return reportLocation && inEvent + } + + /** + * Handle opening the file path that MAGE was launched with + */ + private fun handleOpenFilePath(path: String) { + val cacheFile = File(path) + + // Handle GeoPackage files by linking them to their current location + if (GeoPackageValidate.hasGeoPackageExtension(cacheFile)) { + val event = eventLocalDataSource.currentEvent + val cacheName = GeoPackageCacheUtils.importGeoPackage(applicationContext, cacheFile) + if (event != null && cacheName != null) { + CoroutineScope(Dispatchers.IO).launch { + cacheProvider.enableAndRefreshTileOverlays(cacheName) + } + } + } + } + + companion object { + private const val PERMISSIONS_REQUEST_ACCESS_STORAGE = 100 + private const val CHANGE_EVENT_REQUEST = 200 + + const val EXTRA_OPEN_FILE_PATH = "extra_open_file_path" + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/LandingViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/LandingViewModel.kt index 8867b6c53..c105c3a3e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/LandingViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/LandingViewModel.kt @@ -6,15 +6,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import mil.nga.giat.mage.data.feed.Feed -import mil.nga.giat.mage.data.feed.FeedDao -import mil.nga.giat.mage.data.feed.FeedItemDao +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.dao.feed.FeedItemDao import mil.nga.giat.mage.map.FeedItemId import mil.nga.giat.mage.map.annotation.MapAnnotation -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.data.repository.user.UserRepository import mil.nga.sf.Geometry import javax.inject.Inject @@ -22,18 +23,18 @@ import javax.inject.Inject class LandingViewModel @Inject constructor( private val application: Application, private val feedDao: FeedDao, - private val feedItemDao: FeedItemDao + private val feedItemDao: FeedItemDao, + private val userRepository: UserRepository, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val locationLocalDataSource: LocationLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource ): ViewModel() { enum class NavigationTab { MAP, OBSERVATIONS, PEOPLE } enum class NavigableType { OBSERVATION, USER, FEED, OTHER } data class Navigable(val id: T, val type: NavigableType, val geometry: Geometry, val icon: Any?) - val userHelper: UserHelper = UserHelper.getInstance(application) - val eventHelper: EventHelper = EventHelper.getInstance(application) - val locationHelper: LocationHelper = LocationHelper.getInstance(application) - val observationHelper: ObservationHelper = ObservationHelper.getInstance(application) - private val _filterText = MutableLiveData() val filterText: LiveData = _filterText fun setFilterText(subtitle: String) { @@ -47,7 +48,7 @@ class LandingViewModel @Inject constructor( } private val eventId = MutableLiveData() - val feeds: LiveData> = Transformations.switchMap(eventId) { + val feeds: LiveData> = eventId.switchMap { feedDao.feedsLiveData(it) } @@ -66,15 +67,31 @@ class LandingViewModel @Inject constructor( setNavigationTab(NavigationTab.MAP) viewModelScope.launch(Dispatchers.IO) { - val observation = observationHelper.read(id) - _navigateTo.postValue( - Navigable( - id.toString(), - NavigableType.OBSERVATION, - observation.geometry, - MapAnnotation.fromObservation(observation, application) + eventLocalDataSource.currentEvent?.let { event -> + val observation = observationLocalDataSource.read(id) + val observationForm = observation.forms.firstOrNull() + val formDefinition = observationForm?.formId?.let { + eventLocalDataSource.getForm(it) + } + + val icon = MapAnnotation.fromObservation( + event = event, + formDefinition = formDefinition, + observationForm = observationForm, + geometryType = observation.geometry.geometryType, + observation = observation, + context = application ) - ) + + _navigateTo.postValue( + Navigable( + id = id.toString(), + type = NavigableType.OBSERVATION, + geometry = observation.geometry, + icon = icon + ) + ) + } } } @@ -82,17 +99,18 @@ class LandingViewModel @Inject constructor( setNavigationTab(NavigationTab.MAP) viewModelScope.launch(Dispatchers.IO) { - val user = userHelper.read(id) - val event = eventHelper.currentEvent - val location = locationHelper.getUserLocations(user.id, event.id, 1, true).first() - _navigateTo.postValue( - Navigable( - id.toString(), - NavigableType.USER, - location.geometry, - MapAnnotation.fromUser(user, location) + val user = userLocalDataSource.read(id) + eventLocalDataSource.currentEvent?.let { event -> + val location = locationLocalDataSource.getUserLocations(user.id, event.id, 1, true).first() + _navigateTo.postValue( + Navigable( + id.toString(), + NavigableType.USER, + location.geometry, + MapAnnotation.fromUser(user, location) + ) ) - ) + } } } diff --git a/mage/src/main/java/mil/nga/giat/mage/MageApplication.java b/mage/src/main/java/mil/nga/giat/mage/MageApplication.java deleted file mode 100644 index 63ca00934..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/MageApplication.java +++ /dev/null @@ -1,376 +0,0 @@ -package mil.nga.giat.mage; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Application; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.content.ContextCompat; -import androidx.hilt.work.HiltWorkerFactory; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; -import androidx.lifecycle.ProcessLifecycleOwner; -import androidx.work.Configuration; -import androidx.work.Constraints; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; - -import javax.inject.Inject; - -import dagger.hilt.android.HiltAndroidApp; -import kotlin.Unit; -import mil.nga.giat.mage.feed.FeedFetchService; -import mil.nga.giat.mage.location.LocationFetchService; -import mil.nga.giat.mage.location.LocationReportingService; -import mil.nga.giat.mage.login.AccountStateActivity; -import mil.nga.giat.mage.login.LoginActivity; -import mil.nga.giat.mage.login.ServerUrlActivity; -import mil.nga.giat.mage.login.SignupActivity; -import mil.nga.giat.mage.login.idp.IdpLoginActivity; -import mil.nga.giat.mage.network.Server; -import mil.nga.giat.mage.observation.ObservationNotificationListener; -import mil.nga.giat.mage.observation.sync.AttachmentSyncListener; -import mil.nga.giat.mage.observation.sync.AttachmentSyncWorker; -import mil.nga.giat.mage.observation.sync.ObservationFetchService; -import mil.nga.giat.mage.observation.sync.ObservationFetchWorker; -import mil.nga.giat.mage.observation.sync.ObservationSyncListener; -import mil.nga.giat.mage.observation.sync.ObservationSyncWorker; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureHelper; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.event.ISessionEventListener; -import mil.nga.giat.mage.sdk.exceptions.UserException; -import mil.nga.giat.mage.sdk.fetch.ImageryServerFetch; -import mil.nga.giat.mage.sdk.fetch.StaticFeatureServerFetch; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import mil.nga.giat.mage.sdk.http.resource.UserResource; -import mil.nga.giat.mage.sdk.screen.ScreenChangeReceiver; -import mil.nga.giat.mage.sdk.utils.UserUtility; -import mil.nga.giat.mage.wearable.InitializeMAGEWearBridge; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -@HiltAndroidApp -public class MageApplication extends Application implements Configuration.Provider, LifecycleObserver, SharedPreferences.OnSharedPreferenceChangeListener, ISessionEventListener, Application.ActivityLifecycleCallbacks { - - private static final String LOG_NAME = MageApplication.class.getName(); - - public static final int MAGE_SUMMARY_NOTIFICATION_ID = 100; - public static final int MAGE_ACCOUNT_NOTIFICATION_ID = 101; - public static final int MAGE_OBSERVATION_NOTIFICATION_PREFIX = 10000; - - public static final String MAGE_NOTIFICATION_GROUP = "mil.nga.mage.MAGE_NOTIFICATION_GROUP"; - public static final String MAGE_OBSERVATION_NOTIFICATION_GROUP = "mil.nga.mage.MAGE_OBSERVATION_NOTIFICATION_GROUP"; - public static final String MAGE_NOTIFICATION_CHANNEL_ID = "mil.nga.mage.MAGE_NOTIFICATION_CHANNEL"; - public static final String MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID = "mil.nga.mage.MAGE_OBSERVATION_NOTIFICATION_CHANNEL"; - - public interface OnLogoutListener { - void onLogout(); - } - - private ObservationNotificationListener observationNotificationListener = null; - - private Activity runningActivity; - - @Inject - HiltWorkerFactory workerFactory; - - @Inject - Server server; - - @NonNull - @Override - public Configuration getWorkManagerConfiguration() { - return new Configuration.Builder() - .setWorkerFactory(workerFactory) - .build(); - } - - @Override - public void onCreate() { - super.onCreate(); - - // This ensures the singleton is created with the correct context, which needs to be the - // application context - DaoStore.getInstance(getApplicationContext()); - LayerHelper.getInstance(getApplicationContext()); - StaticFeatureHelper.getInstance(getApplicationContext()); - - new ObservationSyncListener(getApplicationContext(), () -> { - ObservationSyncWorker.Companion.scheduleWork(getApplicationContext()); - return Unit.INSTANCE; - }); - - new AttachmentSyncListener(getApplicationContext(), () -> { - AttachmentSyncWorker.Companion.scheduleWork(getApplicationContext()); - return Unit.INSTANCE; - }); - - ProcessLifecycleOwner.get().getLifecycle().addObserver(this); - - HttpClientManager.initialize(this, server); - - // setup the screen unlock stuff - registerReceiver(ScreenChangeReceiver.getInstance(), new IntentFilter(Intent.ACTION_SCREEN_ON)); - - registerActivityLifecycleCallbacks(this); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - int dayNightTheme = preferences.getInt(getResources().getString(R.string.dayNightThemeKey), getResources().getInteger(R.integer.dayNightThemeDefaultValue)); - AppCompatDelegate.setDefaultNightMode(dayNightTheme); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - NotificationChannel channel = new NotificationChannel(MAGE_NOTIFICATION_CHANNEL_ID,"MAGE", NotificationManager.IMPORTANCE_LOW); - channel.setShowBadge(true); - notificationManager.createNotificationChannel(channel); - - NotificationChannel observationChannel = new NotificationChannel(MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID,"MAGE Observations", NotificationManager.IMPORTANCE_HIGH); - observationChannel.setShowBadge(true); - notificationManager.createNotificationChannel(observationChannel); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_START) - public void onApplicationStart() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - preferences.registerOnSharedPreferenceChangeListener(this); - - HttpClientManager.getInstance().addListener(this); - - // Start fetching and pushing observations and locations - if (!UserUtility.getInstance(getApplicationContext()).isTokenExpired()) { - startFetching(); - } - } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - public void onApplicationStop() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); - preferences.registerOnSharedPreferenceChangeListener(this); - - HttpClientManager.getInstance().removeListener(this); - - destroyFetching(); - } - - public void onLogin() { - //set up observation notifications - if (observationNotificationListener == null) { - observationNotificationListener = new ObservationNotificationListener(getApplicationContext()); - ObservationHelper.getInstance(getApplicationContext()).addListener(observationNotificationListener); - } - - // Start fetching observations and locations - startFetching(); - - ObservationFetchWorker.Companion.beginWork(getApplicationContext()); - - // Pull static layers and features just once - loadOnlineAndOfflineLayers(false, null); - - InitializeMAGEWearBridge.startBridgeIfWearBuild(getApplicationContext()); - } - - private void loadOnlineAndOfflineLayers(final boolean force, final StaticFeatureServerFetch.OnStaticLayersListener listener) { - @SuppressLint("StaticFieldLeak") AsyncTask fetcher = new AsyncTask() { - @Override - protected Void doInBackground(Void... voids) { - StaticFeatureServerFetch staticFeatureServerFetch = new StaticFeatureServerFetch(getApplicationContext()); - try { - staticFeatureServerFetch.fetch(force, listener); - } catch (Exception e) { - e.printStackTrace(); - } - - try { - ImageryServerFetch imageryServerFetch = new ImageryServerFetch(getApplicationContext()); - imageryServerFetch.fetch(); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - } ; - fetcher.execute(); - } - - public void onLogout(Boolean clearTokenInformationAndSendLogoutRequest, final OnLogoutListener logoutListener) { - - if (observationNotificationListener != null) { - ObservationHelper.getInstance(getApplicationContext()).removeListener(observationNotificationListener); - observationNotificationListener = null; - } - - destroyFetching(); - destroyNotification(); - stopLocationService(); - - ObservationFetchWorker.Companion.stopWork(getApplicationContext()); - - if (clearTokenInformationAndSendLogoutRequest) { - UserResource userResource = new UserResource(getApplicationContext()); - userResource.logout(new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (logoutListener != null) { - logoutListener.onLogout(); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - Log.e(LOG_NAME, "Unable to logout from server."); - if (logoutListener != null) { - logoutListener.onLogout(); - } - } - }); - UserUtility.getInstance(getApplicationContext()).clearTokenInformation(); - } else { - if (logoutListener != null) { - logoutListener.onLogout(); - } - } - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putBoolean(getString(R.string.disclaimerAcceptedKey), false).apply(); - - try { - UserHelper userHelper = UserHelper.getInstance(getApplicationContext()); - User user = userHelper.readCurrentUser(); - userHelper.removeCurrentEvent(user); - } catch (UserException e) { - e.printStackTrace(); - } - } - - private void destroyNotification() { - NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(MAGE_SUMMARY_NOTIFICATION_ID); - notificationManager.cancel(MAGE_ACCOUNT_NOTIFICATION_ID); - notificationManager.cancel(ObservationNotificationListener.OBSERVATION_NOTIFICATION_ID); - } - - private void startFetching() { - startService(new Intent(getApplicationContext(), LocationFetchService.class)); - startService(new Intent(getApplicationContext(), ObservationFetchService.class)); - startService(new Intent(getApplicationContext(), FeedFetchService.class)); - } - - /** - * Stop Tasks responsible for fetching Observations and Locations from the server. - */ - private void destroyFetching() { - stopService(new Intent(getApplicationContext(), LocationFetchService.class)); - stopService(new Intent(getApplicationContext(), ObservationFetchService.class)); - stopService(new Intent(getApplicationContext(), FeedFetchService.class)); - } - - public void startLocationService() { - Intent intent = new Intent(getApplicationContext(), LocationReportingService.class); - ContextCompat.startForegroundService(getApplicationContext(), intent); - } - - public void stopLocationService() { - Intent intent = new Intent(getApplicationContext(), LocationReportingService.class); - stopService(intent); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (getString(R.string.reportLocationKey).equalsIgnoreCase(key) && !UserUtility.getInstance(getApplicationContext()).isTokenExpired()) { - boolean reportLocation = sharedPreferences.getBoolean(getString(R.string.reportLocationKey), getResources().getBoolean(R.bool.reportLocationDefaultValue)); - if (reportLocation) { - startLocationService(); - } else { - stopLocationService(); - } - } - } - - @Override - public void onError(Throwable error) {} - - @Override - public void onTokenExpired() { - invalidateSession(runningActivity, true); - } - - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) {} - - @Override - public void onActivityStarted(Activity activity) {} - - @Override - public void onActivityResumed(Activity activity) { - if (UserUtility.getInstance(getApplicationContext()).isTokenExpired()) { - invalidateSession(activity, false); - } - - runningActivity = activity; - } - - @Override - public void onActivityPaused(Activity activity) { - runningActivity = null; - } - - @Override - public void onActivityStopped(Activity activity) {} - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} - - @Override - public void onActivityDestroyed(Activity activity) {} - - private void invalidateSession(Activity activity, Boolean applicationInUse) { - destroyFetching(); - - ObservationFetchWorker.Companion.stopWork(getApplicationContext()); - - // TODO JWT where else is disclaimer accepted set to false. - // Why not set to false if activity resumed onActivityResumed and token is invalid? - PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).edit().putBoolean(getString(R.string.disclaimerAcceptedKey), false).apply(); - - if (!(activity instanceof LoginActivity) && - !(activity instanceof IdpLoginActivity) && - !(activity instanceof AccountStateActivity) && - !(activity instanceof SignupActivity) && - !(activity instanceof ServerUrlActivity)) { - forceLogin(applicationInUse); - } - } - - private void forceLogin(boolean applicationInUse) { - Intent intent = new Intent(this, LoginActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(LoginActivity.EXTRA_CONTINUE_SESSION, true); - intent.putExtra(LoginActivity.EXTRA_CONTINUE_SESSION_WHILE_USING, applicationInUse); - - startActivity(intent); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/MageApplication.kt b/mage/src/main/java/mil/nga/giat/mage/MageApplication.kt new file mode 100644 index 000000000..20e594472 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/MageApplication.kt @@ -0,0 +1,275 @@ +package mil.nga.giat.mage + +import android.app.Activity +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat +import androidx.hilt.work.HiltWorkerFactory +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Observer +import androidx.lifecycle.ProcessLifecycleOwner +import androidx.work.Configuration +import dagger.hilt.android.HiltAndroidApp +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource +import mil.nga.giat.mage.data.repository.layer.LayerRepository +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.di.TokenProvider +import mil.nga.giat.mage.feed.FeedFetchService +import mil.nga.giat.mage.location.LocationFetchService +import mil.nga.giat.mage.location.LocationReportingService +import mil.nga.giat.mage.login.AccountStateActivity +import mil.nga.giat.mage.login.LoginActivity +import mil.nga.giat.mage.login.ServerUrlActivity +import mil.nga.giat.mage.login.SignupActivity +import mil.nga.giat.mage.login.idp.IdpLoginActivity +import mil.nga.giat.mage.network.Server +import mil.nga.giat.mage.observation.ObservationNotificationListener +import mil.nga.giat.mage.observation.sync.AttachmentSyncListener +import mil.nga.giat.mage.observation.sync.AttachmentSyncWorker +import mil.nga.giat.mage.observation.sync.ObservationFetchService +import mil.nga.giat.mage.observation.sync.ObservationFetchWorker +import mil.nga.giat.mage.observation.sync.ObservationSyncListener +import mil.nga.giat.mage.observation.sync.ObservationSyncWorker +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.di.TokenStatus +import javax.inject.Inject + +@HiltAndroidApp +class MageApplication : Application(), + Configuration.Provider, + SharedPreferences.OnSharedPreferenceChangeListener, + LifecycleEventObserver, + Application.ActivityLifecycleCallbacks +{ + private var observationNotificationListener: ObservationNotificationListener? = null + private var runningActivity: Activity? = null + + private var tokenObserver = Observer { tokenStatus -> + if (tokenStatus !is TokenStatus.Active) { + Log.d(LOG_NAME, "Application token expired, remove session") + invalidateSession( + activity = runningActivity, + applicationInUse = tokenStatus is TokenStatus.Expired + ) + } + } + + @Inject lateinit var server: Server + @Inject lateinit var tokenProvider: TokenProvider + @Inject lateinit var workerFactory: HiltWorkerFactory + @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var userRepository: UserRepository + @Inject lateinit var layerRepository: LayerRepository + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var observationLocalDataSource: ObservationLocalDataSource + @Inject lateinit var attachmentLocalDataSource: AttachmentLocalDataSource + + override fun getWorkManagerConfiguration(): Configuration { + return Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + } + + override fun onCreate() { + super.onCreate() + + ObservationSyncListener(observationLocalDataSource) { + ObservationSyncWorker.scheduleWork(applicationContext) + } + AttachmentSyncListener(attachmentLocalDataSource) { + AttachmentSyncWorker.scheduleWork(applicationContext) + } + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + + // setup the screen unlock stuff + registerActivityLifecycleCallbacks(this) + val dayNightTheme: Int = preferences.getInt( + resources.getString(R.string.dayNightThemeKey), + resources.getInteger(R.integer.dayNightThemeDefaultValue) + ) + AppCompatDelegate.setDefaultNightMode(dayNightTheme) + val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel( + MAGE_NOTIFICATION_CHANNEL_ID, + "MAGE", + NotificationManager.IMPORTANCE_LOW + ) + channel.setShowBadge(true) + notificationManager.createNotificationChannel(channel) + val observationChannel = NotificationChannel( + MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID, + "MAGE Observations", + NotificationManager.IMPORTANCE_HIGH + ) + observationChannel.setShowBadge(true) + notificationManager.createNotificationChannel(observationChannel) + } + + fun onLogin() { + //set up observation notifications + if (observationNotificationListener == null) { + val listener = ObservationNotificationListener(applicationContext) + observationLocalDataSource.addListener(listener) + observationNotificationListener = listener + } + + // Start fetching observations and locations + startFetching() + ObservationFetchWorker.beginWork(applicationContext) + } + + + fun onLogout( + clearTokenInformationAndSendLogoutRequest: Boolean + ) { + observationNotificationListener?.let { observationLocalDataSource.removeListener(it) } + observationNotificationListener = null + + destroyFetching() + destroyNotification() + stopLocationService() + ObservationFetchWorker.stopWork(applicationContext) + + if (clearTokenInformationAndSendLogoutRequest) { + userRepository.signout() + } + + preferences.edit() + .putBoolean(getString(R.string.disclaimerAcceptedKey), false) + .apply() + + userLocalDataSource.removeCurrentEvent() + } + + private fun destroyNotification() { + val notificationManager: NotificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(MAGE_SUMMARY_NOTIFICATION_ID) + notificationManager.cancel(MAGE_ACCOUNT_NOTIFICATION_ID) + notificationManager.cancel(ObservationNotificationListener.OBSERVATION_NOTIFICATION_ID) + } + + private fun startFetching() { + startService(Intent(applicationContext, LocationFetchService::class.java)) + startService(Intent(applicationContext, ObservationFetchService::class.java)) + startService(Intent(applicationContext, FeedFetchService::class.java)) + } + + /** + * Stop Tasks responsible for fetching Observations and Locations from the server. + */ + private fun destroyFetching() { + stopService(Intent(applicationContext, LocationFetchService::class.java)) + stopService(Intent(applicationContext, ObservationFetchService::class.java)) + stopService(Intent(applicationContext, FeedFetchService::class.java)) + } + + fun startLocationService() { + val intent = Intent(applicationContext, LocationReportingService::class.java) + ContextCompat.startForegroundService(applicationContext, intent) + } + + fun stopLocationService() { + val intent = Intent(applicationContext, LocationReportingService::class.java) + stopService(intent) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + if (getString(R.string.reportLocationKey).equals(key, ignoreCase = true) && !tokenProvider.isExpired()) { + val reportLocation = sharedPreferences?.getBoolean( + getString(R.string.reportLocationKey), + resources.getBoolean(R.bool.reportLocationDefaultValue) + ) + if (reportLocation == true) { + startLocationService() + } else { + stopLocationService() + } + } + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) { + if (tokenProvider.isExpired()) { + invalidateSession(activity, false) + } + runningActivity = activity + } + + override fun onActivityPaused(activity: Activity) { + runningActivity = null + } + + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} + + private fun invalidateSession(activity: Activity?, applicationInUse: Boolean) { + destroyFetching() + ObservationFetchWorker.stopWork(applicationContext) + + // TODO JWT where else is disclaimer accepted set to false. + // Why not set to false if activity resumed onActivityResumed and token is invalid? + preferences.edit().putBoolean(getString(R.string.disclaimerAcceptedKey), false).apply() + + if (activity !is LoginActivity && + activity !is IdpLoginActivity && + activity !is AccountStateActivity && + activity !is SignupActivity && + activity !is ServerUrlActivity + ) { + forceLogin(applicationInUse) + } + } + + private fun forceLogin(applicationInUse: Boolean) { + val intent = Intent(this, LoginActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.putExtra(LoginActivity.EXTRA_CONTINUE_SESSION, true) + intent.putExtra(LoginActivity.EXTRA_CONTINUE_SESSION_WHILE_USING, applicationInUse) + startActivity(intent) + } + + companion object { + private val LOG_NAME = MageApplication::class.java.name + + const val MAGE_SUMMARY_NOTIFICATION_ID = 100 + const val MAGE_ACCOUNT_NOTIFICATION_ID = 101 + const val MAGE_OBSERVATION_NOTIFICATION_PREFIX = 10000 + const val MAGE_NOTIFICATION_GROUP = "mil.nga.mage.MAGE_NOTIFICATION_GROUP" + const val MAGE_OBSERVATION_NOTIFICATION_GROUP = "mil.nga.mage.MAGE_OBSERVATION_NOTIFICATION_GROUP" + const val MAGE_NOTIFICATION_CHANNEL_ID = "mil.nga.mage.MAGE_NOTIFICATION_CHANNEL" + const val MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID = "mil.nga.mage.MAGE_OBSERVATION_NOTIFICATION_CHANNEL" + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + when (event) { + Lifecycle.Event.ON_START -> { + preferences.registerOnSharedPreferenceChangeListener(this) + + tokenProvider.observe(source, tokenObserver) + + // Start fetching and pushing observations and locations + if (!tokenProvider.isExpired()) { + startFetching() + } + } + Lifecycle.Event.ON_DESTROY -> { + tokenProvider.removeObserver(tokenObserver) + preferences.registerOnSharedPreferenceChangeListener(this) + destroyFetching() + } + else -> {} + } + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.java b/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.java deleted file mode 100644 index 8bafd8521..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package mil.nga.giat.mage.cache; - -import android.content.Context; -import android.net.Uri; -import android.os.Environment; - -import java.io.File; - -import mil.nga.geopackage.GeoPackageConstants; -import mil.nga.geopackage.io.GeoPackageIOUtils; -import mil.nga.geopackage.validate.GeoPackageValidate; -import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.sdk.utils.MediaUtility; - -/** - * Cache File Utilities - */ -public class CacheUtils { - - public static String CACHE_DIRECTORY = "caches"; - - /** - * Copy the Uri to the cache directory in a background task - * - * @param context - * @param uri - * @param path bn - */ - public static void copyToCache(Context context, Uri uri, String path) { - - // Get a cache directory to write to - File cacheDirectory = CacheUtils.getApplicationCacheDirectory(context); - if (cacheDirectory != null) { - - // Get the Uri display name, which should be the file name with extension - String name = MediaUtility.getDisplayName(context, uri, path); - - // If no extension, add a GeoPackage extension - if(GeoPackageIOUtils.getFileExtension(new File(name)) == null){ - name += "." + GeoPackageConstants.EXTENSION; - } - - // Verify that the file is a cache file by its extension - File cacheFile = new File(cacheDirectory, name); - if (isCacheFile(cacheFile)) { - - if(cacheFile.exists()) { - cacheFile.delete(); - } - String cacheName = MediaUtility.getFileNameWithoutExtension(cacheFile); - CacheProvider.getInstance(context).removeCacheOverlay(cacheName); - - // Copy the file in a background task - CopyCacheStreamTask task = new CopyCacheStreamTask(context, uri, cacheFile, cacheName); - task.execute(); - } - } - } - - /** - * Determine if the file is a cache file based upon its extension - * - * @param file potential cache file - * @return true if a cache file - */ - public static boolean isCacheFile(File file) { - return GeoPackageValidate.hasGeoPackageExtension(file); - } - - /** - * Get a writeable cache directory for saving cache files - * - * @param context - * @return file directory or null - */ - public static File getApplicationCacheDirectory(Context context) { - File directory = context.getFilesDir(); - - String state = Environment.getExternalStorageState(); - if (Environment.MEDIA_MOUNTED.equals(state)) { - File externalDirectory = context.getExternalFilesDir(null); - if (externalDirectory != null) { - directory = externalDirectory; - } - } - - File cacheDirectory = new File(directory, CACHE_DIRECTORY); - if (!cacheDirectory.exists()) { - cacheDirectory.mkdir(); - } - - return cacheDirectory; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.kt b/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.kt new file mode 100644 index 000000000..6703511b7 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/cache/CacheUtils.kt @@ -0,0 +1,95 @@ +package mil.nga.giat.mage.cache + +import android.content.Context +import android.net.Uri +import android.os.Environment +import mil.nga.geopackage.GeoPackageConstants +import mil.nga.geopackage.io.GeoPackageIOUtils +import mil.nga.geopackage.validate.GeoPackageValidate +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.sdk.utils.MediaUtility +import java.io.File + +class CacheUtils( + private val context: Context, + private val cacheProvider: CacheProvider +) { + + /** + * Copy the Uri to the cache directory in a background task + * + * @param uri + * @param path + */ + suspend fun copyToCache(uri: Uri, path: String) { + + // Get a cache directory to write to + val cacheDirectory = getApplicationCacheDirectory(context) + + // Get the Uri display name, which should be the file name with extension + var name = MediaUtility.getDisplayName(context, uri, path) + + // If no extension, add a GeoPackage extension + if (GeoPackageIOUtils.getFileExtension(File(name)) == null) { + name += "." + GeoPackageConstants.EXTENSION + } + + // Verify that the file is a cache file by its extension + val cacheFile = File(cacheDirectory, name) + if (isCacheFile(cacheFile)) { + if (cacheFile.exists()) { + cacheFile.delete() + } + val cacheName = MediaUtility.getFileNameWithoutExtension(cacheFile) + cacheProvider.removeCacheOverlay(cacheName) + + copyCacheStream(uri, cacheFile, cacheName) + } + } + + /** + * Determine if the file is a cache file based upon its extension + * + * @param file potential cache file + * @return true if a cache file + */ + private fun isCacheFile(file: File?): Boolean { + return GeoPackageValidate.hasGeoPackageExtension(file) + } + + private suspend fun copyCacheStream(uri: Uri, cacheFile: File, cacheName: String) { + context.contentResolver.openInputStream(uri)?.use { input -> + cacheFile.outputStream().use { output -> + input.copyTo(output) + } + } + + cacheProvider.enableAndRefreshTileOverlays(cacheName) + } + + companion object { + private var CACHE_DIRECTORY = "caches" + + /** + * Get a writeable cache directory for saving cache files + * + * @param context + * @return file directory or null + */ + fun getApplicationCacheDirectory(context: Context): File { + var directory = context.filesDir + val state = Environment.getExternalStorageState() + if (Environment.MEDIA_MOUNTED == state) { + val externalDirectory = context.getExternalFilesDir(null) + if (externalDirectory != null) { + directory = externalDirectory + } + } + val cacheDirectory = File(directory, CACHE_DIRECTORY) + if (!cacheDirectory.exists()) { + cacheDirectory.mkdir() + } + return cacheDirectory + } + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/cache/CopyCacheStreamTask.java b/mage/src/main/java/mil/nga/giat/mage/cache/CopyCacheStreamTask.java deleted file mode 100644 index c65f3e63f..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/cache/CopyCacheStreamTask.java +++ /dev/null @@ -1,89 +0,0 @@ -package mil.nga.giat.mage.cache; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.AsyncTask; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; - -import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.sdk.utils.MediaUtility; - -/** - * Task for copying a cache file Uri stream to the cache folder location - */ -public class CopyCacheStreamTask extends AsyncTask { - - /** - * Context - */ - private Context context; - - /** - * Intent Uri used to launch MAGE - */ - private Uri uri; - - /** - * Cache file to create - */ - private File cacheFile; - - /** - * Cache name - */ - private String cacheName; - - /** - * Constructor - * - * @param context - * @param uri Uri containing stream - * @param cacheFile copy to cache file location - * @param cacheName cache name - */ - public CopyCacheStreamTask(Context context, Uri uri, File cacheFile, String cacheName) { - this.context = context; - this.uri = uri; - this.cacheFile = cacheFile; - this.cacheName = cacheName; - } - - /** - * Copy the cache stream to cache file location - * - * @param params - * @return - */ - @Override - protected String doInBackground(Void... params) { - - String error = null; - - final ContentResolver resolver = context.getContentResolver(); - try { - InputStream stream = resolver.openInputStream(uri); - MediaUtility.copyStream(stream, cacheFile); - } catch (IOException e) { - error = e.getMessage(); - } - - return error; - } - - /** - * Enable the new cache file and refresh the overlays - * - * @param result - */ - @Override - protected void onPostExecute(String result) { - if (result == null) { - CacheProvider.getInstance(context).enableAndRefreshTileOverlays(cacheName); - } - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/compat/server5/form/view/AttachmentViewContentServer5.kt b/mage/src/main/java/mil/nga/giat/mage/compat/server5/form/view/AttachmentViewContentServer5.kt index a4b193a6b..bd899a499 100644 --- a/mage/src/main/java/mil/nga/giat/mage/compat/server5/form/view/AttachmentViewContentServer5.kt +++ b/mage/src/main/java/mil/nga/giat/mage/compat/server5/form/view/AttachmentViewContentServer5.kt @@ -14,7 +14,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -23,10 +22,9 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition -import com.google.accompanist.glide.rememberGlidePainter import mil.nga.giat.mage.glide.transform.VideoOverlayTransformation import mil.nga.giat.mage.observation.edit.AttachmentAction -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.util.* @Composable diff --git a/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/SignupViewModelServer5.kt b/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/SignupViewModelServer5.kt index 9139c18f0..d370b52b2 100644 --- a/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/SignupViewModelServer5.kt +++ b/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/SignupViewModelServer5.kt @@ -1,10 +1,9 @@ package mil.nga.giat.mage.compat.server5.login -import android.content.Context import android.content.SharedPreferences import com.google.gson.JsonObject import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext +import mil.nga.giat.mage.data.repository.user.UserRepository import mil.nga.giat.mage.login.SignupViewModel import retrofit2.Call import retrofit2.Callback @@ -13,12 +12,12 @@ import javax.inject.Inject @HiltViewModel class SignupViewModelServer5 @Inject constructor( - @ApplicationContext context: Context, - preferences: SharedPreferences -): SignupViewModel(context, preferences) { + preferences: SharedPreferences, + private val userResource: UserResourceServer5, + private val userRepository: UserRepository +): SignupViewModel(preferences, userRepository) { fun signup(account: Account) { - val userResource = UserResourceServer5(context) userResource.create(account.username, account.displayName, account.email, account.phone, account.password, object: Callback { override fun onResponse(call: Call, response: Response) { if (response.isSuccessful) { diff --git a/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/UserResourceServer5.kt b/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/UserResourceServer5.kt index ba0dcb5ba..003885f1e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/UserResourceServer5.kt +++ b/mage/src/main/java/mil/nga/giat/mage/compat/server5/login/UserResourceServer5.kt @@ -1,21 +1,20 @@ package mil.nga.giat.mage.compat.server5.login import android.content.Context -import androidx.preference.PreferenceManager import com.google.gson.JsonObject import dagger.hilt.android.qualifiers.ApplicationContext -import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.http.HttpClientManager import retrofit2.Call import retrofit2.Callback import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.Body import retrofit2.http.POST import javax.inject.Inject +import javax.inject.Singleton +@Singleton class UserResourceServer5 @Inject constructor( - @ApplicationContext val context: Context + @ApplicationContext val context: Context, + val retrofit: Retrofit ) { interface UserServiceServer5 { @@ -24,18 +23,10 @@ class UserResourceServer5 @Inject constructor( } @Throws(Exception::class) - fun create(username: String, displayname: String, email: String, phone: String, password: String, callback: Callback) { - - val baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)) - val retrofit = Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(HttpClientManager.getInstance().httpClient()) - .build() - + fun create(username: String, displayName: String, email: String, phone: String, password: String, callback: Callback) { val json = JsonObject() json.addProperty("username", username) - json.addProperty("displayName", displayname) + json.addProperty("displayName", displayName) json.addProperty("email", email) json.addProperty("phone", phone) json.addProperty("password", password) diff --git a/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/FormViewModel_server5.kt b/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/FormViewModel_server5.kt index 0fd9b3a49..232e16f2a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/FormViewModel_server5.kt +++ b/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/FormViewModel_server5.kt @@ -1,10 +1,16 @@ package mil.nga.giat.mage.compat.server5.observation.edit -import android.content.Context +import android.app.Application import android.util.Log import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.observation.State import mil.nga.giat.mage.form.* import mil.nga.giat.mage.form.Form.Companion.fromJson import mil.nga.giat.mage.form.defaults.FormPreferences @@ -14,276 +20,287 @@ import mil.nga.giat.mage.form.field.FieldValue import mil.nga.giat.mage.form.field.GeometryFieldState import mil.nga.giat.mage.observation.* import mil.nga.giat.mage.observation.edit.MediaAction -import mil.nga.giat.mage.sdk.datastore.observation.* -import mil.nga.giat.mage.sdk.datastore.user.Permission -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.permission.Permission +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.exceptions.UserException import java.util.* import javax.inject.Inject @HiltViewModel class FormViewModel_server5 @Inject constructor( - @ApplicationContext context: Context -) : FormViewModel(context) { - - companion object { - private val LOG_NAME = FormViewModel_server5::class.java.name - } - - val attachments = mutableListOf() - - override fun createObservation(timestamp: Date, location: ObservationLocation, defaultMapZoom: Float?, defaultMapCenter: LatLng?): Boolean { - if (_observationState.value != null) return false - - val forms = mutableListOf() - val formDefinitions = mutableListOf
() - event.forms.mapNotNull { form -> - fromJson(form.json) - } - .forEachIndexed { index, form -> - formDefinitions.add(form) - - val defaultForm = FormPreferences(context, event.id, form.id).getDefaults() - repeat((form.min ?: 0) + if (form.default) 1 else 0) { - val formState = FormState.fromForm(eventId = event.remoteId, form = form, defaultForm = defaultForm) - formState.expanded.value = index == 0 - forms.add(formState) - } - } - - val observation = Observation() - _observation.value = observation - observation.event = event - observation.geometry = location.geometry - - var user: User? = null - try { - user = UserHelper.getInstance(context).readCurrentUser() - if (user != null) { - observation.userId = user.remoteId + private val application: Application, + private val userLocalDataSource: UserLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource, + eventLocalDataSource: EventLocalDataSource +) : FormViewModel(application, userLocalDataSource, observationLocalDataSource, eventLocalDataSource) { + + val attachments = mutableListOf() + + override fun createObservation(timestamp: Date, location: ObservationLocation, defaultMapZoom: Float?, defaultMapCenter: LatLng?): Boolean { + if (_observationState.value != null) return false + + val forms = mutableListOf() + val formDefinitions = mutableListOf() + event?.forms?.mapNotNull { form -> + fromJson(form.json) } - } catch (ue: UserException) { } - - val timestampFieldState = DateFieldState( - DateFormField( - id = 0, - type = FieldType.DATE, - name ="timestamp", - title = "Date", - required = true, - archived = false - ) as FormField - ) - timestampFieldState.answer = FieldValue.Date(timestamp) - - val geometryFieldState = GeometryFieldState( - GeometryFormField( - id = 0, - type = FieldType.GEOMETRY, - name = "geometry", - title = "Location", - required = true, - archived = false - ) as FormField, - defaultMapZoom = defaultMapZoom, - defaultMapCenter = defaultMapCenter - ) - geometryFieldState.answer = FieldValue.Location(ObservationLocation(location.geometry)) - - val definition = ObservationDefinition( - minObservationForms = if (formDefinitions.isEmpty()) 0 else 1, - maxObservationForms = 1, - forms = formDefinitions - ) - val observationState = ObservationState( - status = ObservationStatusState(), - definition = definition, - timestampFieldState = timestampFieldState, - geometryFieldState = geometryFieldState, - userDisplayName = user?.displayName, - forms = forms) - _observationState.value = observationState - - return observationState.forms.value.isEmpty() && formDefinitions.isNotEmpty() - } - - override fun createObservationState(observation: Observation, defaultMapZoom: Float?, defaultMapCenter: LatLng?) { - _observation.value = observation - - val formDefinitions = mutableMapOf() - for (form in event.forms) { - fromJson(form.json)?.let { it -> - formDefinitions.put(it.id, it) + ?.forEachIndexed { index, form -> + formDefinitions.add(form) + + val defaultForm = FormPreferences(application, event.id, form.id).getDefaults() + repeat((form.min ?: 0) + if (form.default) 1 else 0) { + val formState = FormState.fromForm(eventId = event.remoteId, form = form, defaultForm = defaultForm) + formState.expanded.value = index == 0 + forms.add(formState) + } + } + + val observation = Observation() + _observation.value = observation + observation.event = event + observation.geometry = location.geometry + + var user: User? = null + try { + user = userLocalDataSource.readCurrentUser() + if (user != null) { + observation.userId = user.remoteId + } + } catch (_: UserException) { } + + val timestampFieldState = DateFieldState( + DateFormField( + id = 0, + type = FieldType.DATE, + name ="timestamp", + title = "Date", + required = true, + archived = false + ) as FormField + ) + timestampFieldState.answer = FieldValue.Date(timestamp) + + val geometryFieldState = GeometryFieldState( + GeometryFormField( + id = 0, + type = FieldType.GEOMETRY, + name = "geometry", + title = "Location", + required = true, + archived = false + ) as FormField, + defaultMapZoom = defaultMapZoom, + defaultMapCenter = defaultMapCenter + ) + geometryFieldState.answer = FieldValue.Location(ObservationLocation(location.geometry)) + + val definition = ObservationDefinition( + minObservationForms = if (formDefinitions.isEmpty()) 0 else 1, + maxObservationForms = 1, + forms = formDefinitions + ) + val observationState = ObservationState( + status = ObservationStatusState(), + definition = definition, + timestampFieldState = timestampFieldState, + geometryFieldState = geometryFieldState, + userDisplayName = user?.displayName, + forms = forms) + _observationState.value = observationState + + return observationState.forms.value.isEmpty() && formDefinitions.isNotEmpty() + } + + override fun createObservationState(observation: Observation, defaultMapZoom: Float?, defaultMapCenter: LatLng?) { + val currentEvent = event ?: return + _observation.value = observation + + val formDefinitions = mutableMapOf() + currentEvent.forms.forEach { form -> + fromJson(form.json)?.let { + formDefinitions[it.id] = it + } } - } - - val forms = mutableListOf() - observation.forms.forEachIndexed { index, observationForm -> - val form = formDefinitions[observationForm.formId] - if (form != null) { - val fields = mutableListOf>() - for (field in form.fields) { - val property = observationForm.properties.find { it.key == field.name } - val fieldState = FieldState.fromFormField(field, property?.value) - fields.add(fieldState) - } - - val formState = FormState(observationForm.id, observationForm.remoteId, event.remoteId, form, fields) - formState.expanded.value = index == 0 - forms.add(formState) + + val forms = mutableListOf() + observation.forms.forEachIndexed { index, observationForm -> + val form = formDefinitions[observationForm.formId] + if (form != null) { + val fields = mutableListOf>() + for (field in form.fields) { + val property = observationForm.properties.find { it.key == field.name } + val fieldState = FieldState.fromFormField(field, property?.value) + fields.add(fieldState) + } + + val formState = FormState(observationForm.id, observationForm.remoteId, event.remoteId, form, fields) + formState.expanded.value = index == 0 + forms.add(formState) + } } - } - - val timestampFieldState = DateFieldState( - DateFormField( - id = 0, - type = FieldType.DATE, - name ="timestamp", - title = "Date", - required = true, - archived = false - ) as FormField - ) - timestampFieldState.answer = FieldValue.Date(observation.timestamp) - - val geometryFieldState = GeometryFieldState( - GeometryFormField( - id = 0, - type = FieldType.GEOMETRY, - name = "geometry", - title = "Location", - required = true, - archived = false - ) as FormField, - defaultMapZoom = defaultMapZoom, - defaultMapCenter = defaultMapCenter - ) - geometryFieldState.answer = FieldValue.Location(ObservationLocation(observation)) - - val user: User? = try { - UserHelper.getInstance(context).read(observation.userId) - } catch (ue: UserException) { null } - - val permissions = mutableSetOf() - val userPermissions: Collection? = user?.role?.permissions?.permissions - if (userPermissions?.contains(Permission.UPDATE_OBSERVATION_ALL) == true || - userPermissions?.contains(Permission.UPDATE_OBSERVATION_EVENT) == true) { - permissions.add(ObservationPermission.EDIT) - } - - if (userPermissions?.contains(Permission.DELETE_OBSERVATION) == true || observation.userId.equals(user)) { - permissions.add(ObservationPermission.DELETE) - } - - if (userPermissions?.contains(Permission.UPDATE_EVENT) == true || hasUpdatePermissionsInEventAcl(user)) { - permissions.add(ObservationPermission.FLAG) - } - - val isFavorite = if (user != null) { - val favorite = observation.favoritesMap[user.remoteId] - favorite != null && favorite.isFavorite - } else false - - val status = ObservationStatusState(observation.isDirty, observation.lastModified, observation.error?.message) - val definition = ObservationDefinition( - minObservationForms = if (forms.isEmpty()) 0 else 1, - maxObservationForms = 1, - forms = formDefinitions.values - ) - - val importantState = if (observation.important?.isImportant == true) { - val importantUser: User? = try { - UserHelper.getInstance(context).read(observation.important?.userId) + + val timestampFieldState = DateFieldState( + DateFormField( + id = 0, + type = FieldType.DATE, + name ="timestamp", + title = "Date", + required = true, + archived = false + ) as FormField + ) + timestampFieldState.answer = FieldValue.Date(observation.timestamp) + + val geometryFieldState = GeometryFieldState( + GeometryFormField( + id = 0, + type = FieldType.GEOMETRY, + name = "geometry", + title = "Location", + required = true, + archived = false + ) as FormField, + defaultMapZoom = defaultMapZoom, + defaultMapCenter = defaultMapCenter + ) + geometryFieldState.answer = FieldValue.Location(ObservationLocation(observation)) + + val user: User? = try { + userLocalDataSource.read(observation.userId) } catch (ue: UserException) { null } - ObservationImportantState( - description = observation.important?.description, - user = importantUser?.displayName + val permissions = mutableSetOf() + val userPermissions: Collection? = user?.role?.permissions?.permissions + if (userPermissions?.contains(Permission.UPDATE_OBSERVATION_ALL) == true || + userPermissions?.contains(Permission.UPDATE_OBSERVATION_EVENT) == true) { + permissions.add(ObservationPermission.EDIT) + } + + if (userPermissions?.contains(Permission.DELETE_OBSERVATION) == true || observation.userId.equals(user)) { + permissions.add(ObservationPermission.DELETE) + } + + if (userPermissions?.contains(Permission.UPDATE_EVENT) == true || hasUpdatePermissionsInEventAcl(user)) { + permissions.add(ObservationPermission.FLAG) + } + + val isFavorite = if (user != null) { + val favorite = observation.favoritesMap[user.remoteId] + favorite != null && favorite.isFavorite + } else false + + val status = ObservationStatusState(observation.isDirty, observation.lastModified, observation.error?.message) + val definition = ObservationDefinition( + minObservationForms = if (forms.isEmpty()) 0 else 1, + maxObservationForms = 1, + forms = formDefinitions.values ) - } else null - - val observationState = ObservationState( - id = observation.id, - status = status, - definition = definition, - permissions = permissions, - timestampFieldState = timestampFieldState, - geometryFieldState = geometryFieldState, - userDisplayName = user?.displayName, - forms = forms, - attachments = observation.attachments, - important = importantState, - favorite = isFavorite) - - _observationState.value = observationState - } - - override fun addAttachment(attachment: Attachment, action: MediaAction?) { - this.attachments.add(attachment) - - val attachments = observationState.value?.attachments?.value?.toMutableList() ?: mutableListOf() - attachments.add(attachment) - - _observationState.value?.attachments?.value = attachments - } - - override fun saveObservation(): Boolean { - val observation = _observation.value!! - - observation.state = State.ACTIVE - observation.isDirty = true - observation.timestamp = observationState.value!!.timestampFieldState.answer!!.date - - val location: ObservationLocation = observationState.value!!.geometryFieldState.answer!!.location - observation.geometry = location.geometry - observation.accuracy = location.accuracy - - var provider = location.provider - if (provider == null || provider.trim { it <= ' ' }.isEmpty()) { - provider = "manual" - } - observation.provider = provider - - if (!"manual".equals(provider, ignoreCase = true)) { - // TODO multi-form, what is locationDelta supposed to represent - observation.locationDelta = location.time.toString() - } - - val observationForms: MutableCollection = ArrayList() - val formsState: List = observationState.value?.forms?.value ?: emptyList() - for (formState in formsState) { - val properties: MutableCollection = ArrayList() - for (fieldState in formState.fields) { - val answer = fieldState.answer - if (answer != null) { - // TODO, attachment field value, how to serialize/deserialize - properties.add(ObservationProperty(fieldState.definition.name, answer.serialize())) - } + + val importantState = if (observation.important?.isImportant == true) { + val importantUser = try { + observation.important?.userId?.let { userId -> + userLocalDataSource.read(userId) + } + } catch (e: UserException) { null } + + ObservationImportantState( + description = observation.important?.description, + user = importantUser?.displayName + ) + } else null + + val observationState = ObservationState( + id = observation.id, + status = status, + definition = definition, + permissions = permissions, + timestampFieldState = timestampFieldState, + geometryFieldState = geometryFieldState, + userDisplayName = user?.displayName, + forms = forms, + attachments = observation.attachments, + important = importantState, + favorite = isFavorite) + + _observationState.value = observationState + } + + override fun addAttachment(attachment: Attachment, action: MediaAction?) { + this.attachments.add(attachment) + + val attachments = observationState.value?.attachments?.value?.toMutableList() ?: mutableListOf() + attachments.add(attachment) + + _observationState.value?.attachments?.value = attachments + } + + override fun saveObservation(): Boolean { + val observation = _observation.value!! + + observation.state = State.ACTIVE + observation.isDirty = true + observation.timestamp = observationState.value!!.timestampFieldState.answer!!.date + + val location: ObservationLocation = observationState.value!!.geometryFieldState.answer!!.location + observation.geometry = location.geometry + observation.accuracy = location.accuracy + + var provider = location.provider + if (provider == null || provider.trim { it <= ' ' }.isEmpty()) { + provider = "manual" + } + observation.provider = provider + + if (!"manual".equals(provider, ignoreCase = true)) { + // TODO multi-form, what is locationDelta supposed to represent + observation.locationDelta = location.time.toString() } - val observationForm = ObservationForm() - observationForm.remoteId = formState.remoteId - observationForm.formId = formState.definition.id - observationForm.addProperties(properties) - observationForms.add(observationForm) - } - - observation.forms = observationForms - observation.attachments.addAll(attachments) - - try { - if (observation.id == null) { - val newObs = ObservationHelper.getInstance(context).create(observation) - Log.i(LOG_NAME, "Created new observation with id: " + newObs.id) - } else { - ObservationHelper.getInstance(context).update(observation) - Log.i(LOG_NAME, "Updated observation with remote id: " + observation.remoteId) + val observationForms: MutableCollection = ArrayList() + val formsState: List = observationState.value?.forms?.value ?: emptyList() + for (formState in formsState) { + val properties: MutableCollection = ArrayList() + for (fieldState in formState.fields) { + val answer = fieldState.answer + if (answer != null) { + // TODO, attachment field value, how to serialize/deserialize + properties.add( + ObservationProperty( + fieldState.definition.name, + answer.serialize() + ) + ) + } + } + + val observationForm = + ObservationForm() + observationForm.remoteId = formState.remoteId + observationForm.formId = formState.definition.id + observationForm.addProperties(properties) + observationForms.add(observationForm) } - } catch (e: java.lang.Exception) { - Log.e(LOG_NAME, e.message, e) - } - return true - } + observation.forms = observationForms + observation.attachments.addAll(attachments) + + try { + if (observation.id == null) { + val newObs = observationLocalDataSource.create(observation) + Log.i(LOG_NAME, "Created new observation with id: " + newObs?.id) + } else { + observationLocalDataSource.update(observation) + Log.i(LOG_NAME, "Updated observation with remote id: " + observation.remoteId) + } + } catch (e: java.lang.Exception) { + Log.e(LOG_NAME, e.message, e) + } + + return true + } + + companion object { + private val LOG_NAME = FormViewModel_server5::class.java.name + } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/ObservationResource_sever5.kt b/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/ObservationResource_sever5.kt index 25e7ea5ef..22882909b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/ObservationResource_sever5.kt +++ b/mage/src/main/java/mil/nga/giat/mage/compat/server5/observation/edit/ObservationResource_sever5.kt @@ -2,7 +2,7 @@ package mil.nga.giat.mage.compat.server5.observation.edit import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import okhttp3.RequestBody import retrofit2.Call import retrofit2.http.Multipart diff --git a/mage/src/main/java/mil/nga/giat/mage/contact/ContactDialog.kt b/mage/src/main/java/mil/nga/giat/mage/contact/ContactDialog.kt index 18c5f3e50..a4e76a7ac 100644 --- a/mage/src/main/java/mil/nga/giat/mage/contact/ContactDialog.kt +++ b/mage/src/main/java/mil/nga/giat/mage/contact/ContactDialog.kt @@ -1,6 +1,5 @@ package mil.nga.giat.mage.contact -import android.R import android.content.Context import android.content.SharedPreferences import android.net.Uri @@ -23,14 +22,24 @@ class ContactDialog( this.authenticationStrategy = authenticationStrategy } - fun show() { - val dialog = AlertDialog.Builder(context) + fun show( + workOffline: ((Boolean) -> Unit)? = null + ) { + val builder = AlertDialog.Builder(context) .setTitle(title) .setMessage(addLinks()) - .setPositiveButton(R.string.ok, null) - .show() + .setPositiveButton(android.R.string.ok) { _, _ -> + workOffline?.invoke(false) + } - dialog.findViewById(R.id.message)?.movementMethod = LinkMovementMethod.getInstance() + workOffline?.let { callback -> + builder.setNegativeButton("Work Offline") { _, _ -> + callback(true) + } + } + + val dialog = builder.show() + dialog.findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() } private fun addLinks(): Spanned { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/converters/GeometryTypeConverter.kt b/mage/src/main/java/mil/nga/giat/mage/data/converters/GeometryTypeConverter.kt deleted file mode 100644 index 116a4e655..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/converters/GeometryTypeConverter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package mil.nga.giat.mage.data.converters - -import androidx.room.TypeConverter -import com.google.gson.InstanceCreator -import com.mapbox.geojson.Feature -import com.mapbox.geojson.Point -import mil.nga.giat.mage.sdk.utils.GeometryUtility -import mil.nga.sf.Geometry -import mil.nga.sf.util.GeometryUtils -import java.lang.reflect.Type - -class GeometryTypeConverter { - - @TypeConverter - fun fromByteArray(value: ByteArray?): Geometry? { - return value?.let { - GeometryUtility.toGeometry(value) - } - } - - @TypeConverter - fun fromGeometry(geometry: Geometry?): ByteArray? { - return geometry?.let { - GeometryUtility.toGeometryBytes(it) - } - } - -} -class GeometryInstanceCreator: InstanceCreator { - override fun createInstance(type: Type?): Geometry { - TODO("Not yet implemented") - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/event/EventLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/event/EventLocalDataSource.kt new file mode 100644 index 000000000..847f67f27 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/event/EventLocalDataSource.kt @@ -0,0 +1,209 @@ +package mil.nga.giat.mage.data.datasource.event + +import android.util.Log +import com.j256.ormlite.dao.Dao +import com.j256.ormlite.misc.TransactionManager +import mil.nga.giat.mage.database.model.event.Form.Companion.getColumnNameEventId +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.sdk.exceptions.EventException +import mil.nga.giat.mage.sdk.exceptions.UserException +import org.apache.commons.lang3.StringUtils +import java.sql.SQLException +import javax.inject.Inject + +class EventLocalDataSource @Inject constructor( + private val daoStore: MageSqliteOpenHelper, + private val formDao: Dao, + private val eventDao: Dao, + private val teamEventDao: Dao, + private val userLocalDataSource: UserLocalDataSource, + private val locationLocalDataSource: LocationLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource +) { + + @Throws(EventException::class) + fun create(pEvent: Event): Event { + return try { + eventDao.createIfNotExists(pEvent) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating event: $pEvent", e) + throw EventException("There was a problem creating event: $pEvent", e) + } + } + + @Throws(EventException::class) + fun read(id: Long): Event { + return try { + eventDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw EventException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(EventException::class) + fun readAll(): MutableList { + val events: MutableList = ArrayList() + try { + events.addAll(eventDao.queryForAll()) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Events", e) + throw EventException("Unable to read Events.", e) + } + return events + } + + @Throws(EventException::class) + fun read(pRemoteId: String): Event? { + var event: Event? = null + try { + val results = eventDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + if (results != null && results.size > 0) { + event = results[0] + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", e) + throw EventException("Unable to query for existence for remote_id = '$pRemoteId'", e) + } + return event + } + + @Throws(EventException::class) + fun update(event: Event): Event { + try { + TransactionManager.callInTransaction(daoStore.connectionSource) { + val deleteBuilder = formDao.deleteBuilder() + deleteBuilder.where().eq(getColumnNameEventId(), event.id) + deleteBuilder.delete() + eventDao.update(event) + for (form in event.forms) { + form.event = event + formDao.create(form) + } + null + } + } catch (sqle: SQLException) { + Log.e(LOG_NAME, "There was a problem creating event: $event") + throw EventException("There was a problem creating event: $event", sqle) + } + return event + } + + fun createOrUpdate(event: Event): Event { + return try { + val oldEvent = read(event.remoteId) + if (oldEvent == null) { + val newEvent = create(event) + for (form in newEvent.forms) { + form.event = newEvent + formDao.create(form) + } + Log.d(LOG_NAME, "Created event with remote_id " + newEvent.remoteId) + newEvent + } else { + event.id = oldEvent.id + update(event) + Log.d(LOG_NAME, "Updated event with remote_id " + event.remoteId) + event + } + } catch (e: Exception) { + Log.e(LOG_NAME, "There was a problem creating event: $event", e) + throw EventException("There was a problem creating event: $event", e) + } + + } + + fun getForm(formId: Long): Form? { + var form: Form? = null + try { + val forms = formDao.queryBuilder() + .where() + .eq("formId", formId) + .query() + if (forms != null && forms.size > 0) { + form = forms[0] + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Error pulling form with id: $formId", e) + } + return form + } + + val currentEvent: Event? + get() { + var event: Event? = null + try { + val user: User? = userLocalDataSource.readCurrentUser() + if (user != null) { + event = user.userLocal.currentEvent + } else { + Log.d(LOG_NAME, "Current user is null. Why?") + } + } catch (ue: UserException) { + Log.e(LOG_NAME, "There is no current user. ", ue) + } + return event + } + + fun getRecentEvents(): List { + val user = userLocalDataSource.readCurrentUser() ?: return emptyList() + + return try { + val recentEventIds = user.recentEventIds + val cases = mutableListOf() + for (i in recentEventIds.indices) { + cases.add("WHEN " + recentEventIds[i] + " THEN " + i) + } + eventDao + .queryBuilder() + .orderByRaw( + String.format( + "CASE %s %s END", + Event.COLUMN_NAME_REMOTE_ID, + StringUtils.join(cases, " ") + ) + ) + .where() + .`in`(Event.COLUMN_NAME_REMOTE_ID, user.recentEventIds) + .query() + } catch (e: Exception) { + emptyList() + } + } + + /** + * Remove any events from the database that are not in this event list. + * + * @param remoteEvents list of events that should remain in the database, all others will be removed + */ + fun syncEvents(remoteEvents: List) { + try { + val eventsToRemove = readAll() + eventsToRemove.removeAll(remoteEvents) + for (eventToRemove in eventsToRemove) { + Log.e(LOG_NAME, "Removing event " + eventToRemove.name) + locationLocalDataSource.deleteLocations(eventToRemove) + observationLocalDataSource.deleteObservations(eventToRemove) + val teamDeleteBuilder = teamEventDao.deleteBuilder() + teamDeleteBuilder.where().eq("event_id", eventToRemove.id) + teamDeleteBuilder.delete() + val eventDeleteBuilder = eventDao.deleteBuilder() + eventDeleteBuilder.where().idEq(eventToRemove.id) + eventDeleteBuilder.delete() + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error deleting event ", e) + } + } + + companion object { + private val LOG_NAME = EventLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/feature/FeatureLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/feature/FeatureLocalDataSource.kt new file mode 100644 index 000000000..6744d0417 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/feature/FeatureLocalDataSource.kt @@ -0,0 +1,179 @@ +package mil.nga.giat.mage.data.datasource.feature + +import android.util.Log +import com.j256.ormlite.dao.Dao +import com.j256.ormlite.misc.TransactionManager +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.feature.StaticFeatureProperty +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.sdk.event.IEventDispatcher +import mil.nga.giat.mage.sdk.event.IStaticFeatureEventListener +import mil.nga.giat.mage.sdk.exceptions.StaticFeatureException +import java.sql.SQLException +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FeatureLocalDataSource @Inject constructor( + private val daoStore: MageSqliteOpenHelper, + private val featureDao: Dao, + private val featurePropertyDao: Dao +): IEventDispatcher { + private val listeners: MutableCollection = CopyOnWriteArrayList() + + @Throws(StaticFeatureException::class) + fun create(pStaticFeature: StaticFeature): StaticFeature { + val createdStaticFeature: StaticFeature + try { + createdStaticFeature = featureDao.createIfNotExists(pStaticFeature) + val properties = pStaticFeature.properties + if (properties != null) { + for (property in properties) { + property.staticFeature = createdStaticFeature + featurePropertyDao.create(property) + } + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the static feature: $pStaticFeature.", e) + throw StaticFeatureException("There was a problem creating the static feature: $pStaticFeature.", e) + } + return createdStaticFeature + } + + /** + * Set of layers that features were added to, or already belonged to. + * + * @param staticFeatures + * @return + * @throws StaticFeatureException + */ + @Throws(StaticFeatureException::class) + fun createAll(staticFeatures: Collection, pLayer: Layer): Layer { + try { + TransactionManager.callInTransaction(daoStore.connectionSource) { + for (staticFeature2 in staticFeatures) { + try { + val properties = staticFeature2.properties + val newStaticFeature = featureDao.createIfNotExists(staticFeature2) + + if (properties != null) { + for (property in properties) { + property.staticFeature = newStaticFeature + featurePropertyDao.create(property) + } + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the static feature: $staticFeature2.", e) + continue + } + } + null + } + pLayer.isLoaded = true + for (listener in listeners) { + listener.onStaticFeaturesCreated(pLayer) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating static features.", e) + } + return pLayer + } + + @Throws(StaticFeatureException::class) + fun read(id: Long): StaticFeature { + return try { + featureDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw StaticFeatureException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(StaticFeatureException::class) + fun read(pRemoteId: String): StaticFeature { + var staticFeature: StaticFeature? = null + try { + val results = featureDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + if (results != null && results.size > 0) { + staticFeature = results[0] + } + } catch (sqle: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", sqle) + throw StaticFeatureException( + "Unable to query for existence for remote_id = '$pRemoteId'", + sqle + ) + } + return staticFeature!! + } + + @Throws(StaticFeatureException::class) + fun readAll(pLayerId: Long): List { + val staticFeatures: MutableList = ArrayList() + try { + val results = featureDao.queryBuilder().where().eq("layer_id", pLayerId).query() + if (results != null) { + staticFeatures.addAll(results) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for features with layer id = '$pLayerId'", e) + throw StaticFeatureException("Unable to query for features with layer id = '$pLayerId'", e) + } + return staticFeatures + } + + @Throws(StaticFeatureException::class) + fun readFeature(layerId: Long, id: Long): StaticFeature? { + return try { + val results = featureDao.queryBuilder() + .where() + .eq(StaticFeature.STATIC_FEATURE_LAYER_ID, layerId) + .and() + .eq(StaticFeature.STATIC_FEATURE_ID, id) + .query() + results.firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for feature with layer id: $layerId and id: $id", e) + throw StaticFeatureException("Unable to query for feature with layer id: $layerId and id: $id", e) + } + } + + @Throws(StaticFeatureException::class) + fun deleteAll(layerId: Long) { + val features = readAll(layerId) + val ids: MutableCollection = ArrayList(features.size) + for (feature in features) { + ids.add(feature.id) + } + try { + // Delete the properties (children) + val propertyDeleteBuilder = featurePropertyDao.deleteBuilder() + propertyDeleteBuilder.where().`in`(StaticFeatureProperty.STATIC_FEATURE_ID, ids) + val propertiesDeleted = featurePropertyDao.delete(propertyDeleteBuilder.prepare()) + Log.i(LOG_NAME, "$propertiesDeleted static feature properties deleted") + + // All children deleted, delete the static feature. + val featureDeleteBuilder = featureDao.deleteBuilder() + featureDeleteBuilder.where().eq(StaticFeature.STATIC_FEATURE_LAYER_ID, layerId) + featureDao.delete(featureDeleteBuilder.prepare()) + Log.i(LOG_NAME, "$featureDeleteBuilder features deleted") + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to delete Static Feature: $ids", e) + throw StaticFeatureException("Unable to delete Static Feature: $ids", e) + } + } + + override fun addListener(listener: IStaticFeatureEventListener): Boolean { + return listeners.add(listener) + } + + override fun removeListener(listener: IStaticFeatureEventListener): Boolean { + return listeners.remove(listener) + } + + companion object { + private val LOG_NAME = FeatureLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/layer/LayerLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/layer/LayerLocalDataSource.kt new file mode 100644 index 000000000..bf6cb5102 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/layer/LayerLocalDataSource.kt @@ -0,0 +1,139 @@ +package mil.nga.giat.mage.data.datasource.layer + +import android.util.Log +import com.j256.ormlite.dao.Dao +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.feature.FeatureLocalDataSource +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.sdk.exceptions.LayerException +import java.sql.SQLException +import javax.inject.Inject + +class LayerLocalDataSource @Inject constructor( + private val layerDao: Dao, + private val featureLocalDataSource: FeatureLocalDataSource +) { + + @Throws(LayerException::class) + fun readAll(type: String): List { + return try { + layerDao.queryBuilder().where().eq("type", type).query() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Layers", e) + throw LayerException("Unable to read Layers.", e) + } + } + + fun readByEvent(event: Event?, type: String? = null): List { + event ?: return emptyList() + + return try { + val where = layerDao.queryBuilder().where().eq("event_id", event.id) + type?.let { where.and().eq("type", it) } + where.query() + } catch (e: Exception) { + Log.e(LOG_NAME, "Unable to read Layers", e) + emptyList() + } + } + + @Throws(LayerException::class) + fun read(id: Long): Layer { + return try { + layerDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw LayerException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(LayerException::class) + fun read(pRemoteId: String): Layer? { + return try { + layerDao.queryBuilder() + .where() + .eq("remote_id", pRemoteId) + .query() + .firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", e) + throw LayerException("Unable to query for existence for remote_id = '$pRemoteId'", e) + } + } + + @Throws(LayerException::class) + fun create(pLayer: Layer): Layer { + return try { + layerDao.createIfNotExists(pLayer) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the layer: $pLayer.", e) + throw LayerException("There was a problem creating the layer: $pLayer.", e) + } + } + + @Throws(LayerException::class) + fun update(layer: Layer): Layer { + return try { + layerDao.update(layer) + layer + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem updating layer: $layer") + throw LayerException("There was a problem updating layer: $layer", e) + } + } + + @Throws(LayerException::class) + fun getByRelativePath(relativePath: String): Layer? { + return try { + layerDao.queryBuilder() + .where() + .eq("relative_path", relativePath) + .query() + .firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for relativePath = '$relativePath'", e) + throw LayerException("Unable to query for existence for relativePath = '$relativePath'", e) + } + } + + @Throws(LayerException::class) + fun getByDownloadId(downloadId: Long): Layer? { + return try { + layerDao.queryBuilder() + .where() + .eq("download_id", downloadId) + .query() + .firstOrNull() + } catch (e: SQLException) { + throw LayerException("Unable to query Layer by download id = '$downloadId'", e) + } + } + + @Throws(LayerException::class) + fun delete(id: Long) { + try { + layerDao.queryForId(id)?.let { layer -> + featureLocalDataSource.deleteAll(layer.id) + layerDao.deleteById(id) + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Unable to delete layer: $id", e) + throw LayerException("Unable to delete layer: $id", e) + } + } + + @Throws(LayerException::class) + fun deleteAll(type: String) { + try { + layerDao.queryForAll() + .filter { it.type == type} + .forEach { delete(it.id) } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Error deleting layers") + } + } + + companion object { + private val LOG_NAME = LayerLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/location/LocationLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/location/LocationLocalDataSource.kt new file mode 100644 index 000000000..eaab2eaff --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/location/LocationLocalDataSource.kt @@ -0,0 +1,322 @@ +package mil.nga.giat.mage.data.datasource.location + +import android.util.Log +import com.j256.ormlite.dao.Dao +import com.j256.ormlite.misc.TransactionManager +import com.j256.ormlite.stmt.Where +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.location.LocationProperty +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.filter.Filter +import mil.nga.giat.mage.sdk.Temporal +import mil.nga.giat.mage.sdk.event.IEventDispatcher +import mil.nga.giat.mage.sdk.event.ILocationEventListener +import mil.nga.giat.mage.sdk.exceptions.LocationException +import java.sql.SQLException +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject +import javax.inject.Singleton + +/** + * A utility class for accessing [Location] data from the physical data + * model. The details of ORM DAOs and Lazy Loading should not be exposed past + * this class. + */ +@Singleton +class LocationLocalDataSource @Inject constructor( + private val daoStore: MageSqliteOpenHelper, + private val locationDao: Dao, + private val locationPropertyDao: Dao +) : IEventDispatcher { + private val listeners: MutableCollection = CopyOnWriteArrayList() + + + @Throws(LocationException::class) + fun create(pLocation: Location): Location { + val createdLocation: Location = try { + TransactionManager.callInTransaction(daoStore.connectionSource) { // create Location geometry. + val createdLocation = locationDao.createIfNotExists(pLocation) + // create Location properties. + val locationProperties: Collection? = pLocation.properties + if (locationProperties != null) { + for (locationProperty in locationProperties) { + locationProperty.setLocation(createdLocation) + locationPropertyDao.create(locationProperty) + } + } + for (listener in listeners) { + listener.onLocationCreated(listOf(createdLocation)) + } + createdLocation + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the location: $pLocation.", e) + throw LocationException("There was a problem creating the location: $pLocation.", e) + } + return createdLocation + } + + @Throws(LocationException::class) + fun read(id: Long): Location { + return try { + locationDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw LocationException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(LocationException::class) + fun read(pRemoteId: String): Location? { + return try { + val results = locationDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + results.firstOrNull() + } catch (sqle: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", sqle) + throw LocationException("Unable to query for existence for remote_id = '$pRemoteId'", sqle) + } + } + + /** + * We have to realign all the foreign ids so the update works correctly + * + * @param location + * @throws LocationException + */ + @Throws(LocationException::class) + fun update(location: Location): Location { + // set all the ids as needed + val pOldLocation = read(location.id) + + // do the update + try { + TransactionManager.callInTransaction(daoStore.connectionSource) { + location.id = pOldLocation.id + + // FIXME : make this run faster? + for (lp in location.properties) { + for (olp in pOldLocation.properties) { + if (lp.key.equals(olp.key, ignoreCase = true)) { + lp.id = olp.id + break + } + } + } + locationDao.update(location) + val properties: Collection? = location.properties + if (properties != null) { + for (property in properties) { + property.location = location + locationPropertyDao.createOrUpdate(property) + } + } + null + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem updating the location: $location.", e) + throw LocationException("There was a problem updating the location: $location.", e) + } + + // fire the event + for (listener in listeners) { + listener.onLocationUpdated(location) + } + return location + } + + /** + * Light-weight query for testing the existence of a location in the local data-store. + * @param location The primary key of the passed in Location object is used for the query. + * @return + */ + fun exists(location: Location): Boolean { + return try { + val locations = locationDao.queryBuilder() + .selectColumns("_id") + .limit(1L) + .where() + .eq("_id", location.id) + .query() + locations.isNotEmpty() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for location = '" + location.id + "'", e) + false + } + } + + fun getCurrentUserLocations(user: User?, limit: Long, includeRemote: Boolean): List { + var locations: List = ArrayList() + if (user != null) { + locations = getUserLocations(user.id, null, limit, includeRemote) + } + return locations + } + + fun getSyncedLocations(user: User, event: Event): List { + val queryBuilder = locationDao.queryBuilder() + queryBuilder + .where() + .eq("user_id", user.id) + .and() + .isNotNull("remote_id").and() + .eq("event_id", event.id) + queryBuilder.orderBy("timestamp", false) + return queryBuilder.query() + } + + fun getAllUsersLocations( + user: User?, + filter: Filter? = null + ): List { + val query = locationDao.queryBuilder() + val where = query.where() + + + if (user != null) { + where + .ne("user_id", user.id) + .and() + .eq("event_id", user.userLocal.currentEvent.id) + } + + filter?.let { + it.query()?.let { filterQuery -> query.join(filterQuery) } + it.and(where) + } + + query.orderBy("timestamp", false) + + return locationDao.query(query.prepare()) + } + + fun getUserLocations( + userId: Long?, + eventId: Long?, + limit: Long, + includeRemote: Boolean + ): List { + var locations: List = ArrayList() + val queryBuilder = locationDao.queryBuilder() + try { + if (limit > 0) { + queryBuilder.limit(limit) + // most recent first! + queryBuilder.orderBy("timestamp", false) + } + val where: Where = queryBuilder.where().eq("user_id", userId) + if (eventId != null) { + where.and().eq("event_id", eventId) + } + if (!includeRemote) { + where.and().isNull("remote_id") + } + locations = locationDao.query(queryBuilder.prepare()) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not get current users Locations.") + } + return locations + } + + /** + * This will delete the user's location(s) that have remote_ids. Locations + * that do NOT have remote_ids have not been sync'ed w/ the server. + * + * @param userLocalId + * The user's local id + * @throws LocationException + */ + @Throws(LocationException::class) + fun deleteUserLocations(userLocalId: String?, keepMostRecent: Boolean, event: Event): Int { + val numberLocationsDeleted: Int = try { + // newest first + val qb = locationDao.queryBuilder().orderBy(Location.COLUMN_NAME_TIMESTAMP, false) + qb.where() + .eq(Location.COLUMN_NAME_USER_ID, userLocalId) + .and() + .eq(Location.COLUMN_NAME_EVENT_ID, event.id) + val locations = qb.query().toMutableList() + + // if we should keep the most recent record, then skip one record. + if (keepMostRecent) { + locations.removeAt(0) + } + delete(locations) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to delete user's locations", e) + throw LocationException("Unable to delete user's locations", e) + } + return numberLocationsDeleted + } + + /** + * This will delete all locations for an event. + * + * @param event + * The event to remove locations for + * @throws LocationException + */ + @Throws(LocationException::class) + fun deleteLocations(event: Event) { + Log.e(LOG_NAME, "Deleting locations for event " + event.name) + try { + val qb = locationDao.queryBuilder() + qb.where().eq("event_id", event.id) + val locations: List = qb.query() + delete(locations) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to delete locations for an event", e) + throw LocationException("Unable to delete locations for an event", e) + } + } + + /** + * Deletes locations. This will also delete a Location's child + * Properties and Geometry data. + * + * @param locations + * @throws LocationException + */ + @Throws(LocationException::class) + fun delete(locations: Collection): Int { + val deletedLocations = try { + TransactionManager.callInTransaction(daoStore.connectionSource) { // read the full Location in + val deletedLocations = mutableListOf() + for (location in locations) { + // delete Location properties. + location.properties?.forEach { property -> + locationPropertyDao.deleteById(property.id) + } + + // finally, delete the Location. + locationDao.deleteById(location.id) + deletedLocations.add(location) + } + + for (listener in listeners) { + listener.onLocationDeleted(deletedLocations) + } + + deletedLocations + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to delete Location: " + locations.toTypedArray().contentToString(), e) + throw LocationException("Unable to delete Location: " + locations.toTypedArray().contentToString(), e) + } + + return deletedLocations.size + } + + override fun addListener(listener: ILocationEventListener): Boolean { + return listeners.add(listener) + } + + override fun removeListener(listener: ILocationEventListener): Boolean { + return listeners.remove(listener) + } + + companion object { + private val LOG_NAME = LocationLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/AttachmentLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/AttachmentLocalDataSource.kt new file mode 100644 index 000000000..3718c0cef --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/AttachmentLocalDataSource.kt @@ -0,0 +1,131 @@ +package mil.nga.giat.mage.data.datasource.observation + +import android.util.Log +import com.j256.ormlite.dao.Dao +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.sdk.event.IAttachmentEventListener +import mil.nga.giat.mage.sdk.event.IEventDispatcher +import mil.nga.giat.mage.sdk.exceptions.ObservationException +import java.sql.SQLException +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class AttachmentLocalDataSource @Inject constructor( + private val attachmentDao: Dao +): IEventDispatcher { + + private val listeners: MutableCollection = CopyOnWriteArrayList() + + @Throws(Exception::class) + fun read(id: Long): Attachment { + return try { + attachmentDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Attachment: $id", e) + throw ObservationException("Unable to read Attachment: $id", e) + } + } + + @Throws(Exception::class) + fun read(remoteId: String): Attachment? { + return try { + val results = attachmentDao.queryBuilder().where().eq("remote_id", remoteId).query() + results.firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Attachment: $remoteId", e) + throw ObservationException("Unable to read Attachment: $remoteId", e) + } + } + + @Throws(SQLException::class) + fun create(attachment: Attachment): Attachment { + try { + val oldAttachment = read(attachment.id) + if (attachment.localPath == null) { + attachment.localPath = oldAttachment.localPath + } + } catch (_: Exception) { } + attachmentDao.createOrUpdate(attachment) + for (listener in listeners) { + listener.onAttachmentCreated(attachment) + } + return attachment + } + + /** + * Persist attachment to database. + * + * @param attachment + * @return the attachment + * @throws SQLException + */ + @Throws(SQLException::class) + fun update(attachment: Attachment): Attachment { + try { + val oldAttachment = read(attachment.id) + if (attachment.localPath == null) { + attachment.localPath = oldAttachment.localPath + } + } catch (_: Exception) { } + attachmentDao.update(attachment) + for (listener in listeners) { + listener.onAttachmentUpdated(attachment) + } + return attachment + } + + /** + * Deletes an Attachment. + * + * @param attachment + * @throws Exception + */ + @Throws(SQLException::class) + fun delete(attachment: Attachment) { + attachmentDao.deleteById(attachment.id) + for (listener in listeners) { + listener.onAttachmentDeleted(attachment) + } + } + + /** + * A List of [Attachment] from the datastore that are dirty (i.e. + * should be synced with the server). + * + * @return + */ + val dirtyAttachments: List + get() { + val qb = attachmentDao.queryBuilder() + val queryBuilder = attachmentDao.queryBuilder() + var attachments: List = ArrayList() + try { + val count: Long = qb.countOf() + queryBuilder.where().eq("dirty", true) + attachments = attachmentDao.query(queryBuilder.prepare()) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not get dirty Attachments.", e) + } + return attachments + } + + override fun addListener(listener: IAttachmentEventListener): Boolean { + return listeners.add(listener) + } + + override fun removeListener(listener: IAttachmentEventListener): Boolean { + return listeners.remove(listener) + } + + fun uploadableAttachment(attachment: Attachment?) { + for (listener in listeners) { + listener.onAttachmentUploadable(attachment) + } + } + + companion object { + private val LOG_NAME = AttachmentLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/ObservationLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/ObservationLocalDataSource.kt new file mode 100644 index 000000000..9a3050346 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/observation/ObservationLocalDataSource.kt @@ -0,0 +1,653 @@ +package mil.nga.giat.mage.data.datasource.observation + +import android.app.Application +import android.util.Log +import com.j256.ormlite.dao.Dao +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.observation.State +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.filter.Filter +import mil.nga.giat.mage.sdk.Compatibility.Companion.isServerVersion5 +import mil.nga.giat.mage.sdk.Temporal +import mil.nga.giat.mage.sdk.event.IEventDispatcher +import mil.nga.giat.mage.sdk.event.IObservationEventListener +import mil.nga.giat.mage.sdk.exceptions.ObservationException +import java.sql.SQLException +import java.util.Date +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ObservationLocalDataSource @Inject constructor( + private val application: Application, + private val observationDao: Dao, + private val observationFormDao: Dao, + private val observationPropertyDao: Dao, + private val observationImportantDao: Dao, + private val observationFavoriteDao: Dao, + private val attachmentLocalDataSource: AttachmentLocalDataSource +) : IEventDispatcher { + + private val listeners: MutableCollection = CopyOnWriteArrayList() + + @JvmOverloads + @Throws(ObservationException::class) + fun create(observation: Observation, sendNotifications: Boolean? = true): Observation? { + var savedObservation: Observation? = null + try { + savedObservation = observationDao.callBatchTasks { + + // Now we try and create the Observation structure. + try { + // set last Modified + if (observation.lastModified == null) { + observation.lastModified = Date() + } + + // create the Observation. + observationDao.create(observation) + observation.forms.forEach { form -> + form.setObservation(observation) + observationFormDao.create(form) + + // create Observation properties. + form.properties.forEach { property -> + property.setObservationForm(form) + observationPropertyDao.create(property) + } + } + + // create Observation favorites. + observation.favorites.forEach { favorite -> + favorite.observation = observation + observationFavoriteDao.create(favorite) + } + + // create Observation attachments. + observation.attachments.forEach { attachment -> + try { + attachment.observation = observation + attachmentLocalDataSource.create(attachment) + } catch (e: Exception) { + throw ObservationException("There was a problem creating the observations attachment: $attachment.", e) + } + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the observation: $observation.", e) + throw ObservationException("There was a problem creating the observation: $observation.", e) + } + + // fire the event + for (listener in listeners) { + listener.onObservationCreated(listOf(observation), sendNotifications) + } + observation + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error creating observation", e) + } + return savedObservation + } + + @Throws(ObservationException::class) + fun read(id: Long): Observation { + return try { + observationDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw ObservationException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(ObservationException::class) + fun read(pRemoteId: String): Observation? { + return try { + val results = observationDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + results.firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", e) + throw ObservationException("Unable to query for existence for remote_id = '$pRemoteId'", e) + } + } + + /** + * We have to realign all the foreign ids so the update works correctly + * + * @param observation + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun update(observation: Observation): Observation { + Log.i(LOG_NAME, "Updating observation w/ id: " + observation.id) + val updatedObservation: Observation + try { + updatedObservation = observationDao.callBatchTasks { + + // set all the ids as needed + val oldObservation = read(observation.id) + + // if the observation is dirty, set the last_modified date! + // FIXME this is a server property and should not be set by the client, + // investigate why we are setting this + if (observation.isDirty) { + observation.lastModified = Date() + } + val important = observation.important + val oldImportant = oldObservation.important + if (oldImportant != null && oldImportant.isDirty) { + observation.setImportant(oldImportant) + } else { + if (important != null) { + if (oldImportant != null) { + important.id = oldImportant.id + } + observationImportantDao.createOrUpdate(important) + } else { + if (oldImportant != null) { + observationImportantDao.deleteById(oldImportant.id) + } + } + } + observationDao.update(observation) + + // TODO might not need to delete all forms/properties when server sets a unique form id + // Delete all forms for this observation and all properties + oldObservation.forms.forEach { form -> + form.properties.forEach { property -> + observationPropertyDao.deleteById(property.id) + } + observationFormDao.deleteById(form.id) + } + + observation.forms.forEach { form -> + form.setObservation(observation) + observationFormDao.createOrUpdate(form) + form.properties.forEach { property -> + property.setObservationForm(form) + observationPropertyDao.createOrUpdate(property) + } + } + + val favorites = observation.favoritesMap + val oldFavorites = oldObservation.favoritesMap + favorites.keys.intersect(oldFavorites.keys).forEach { key -> + favorites[key]!!.id = oldFavorites[key]!!.id + } + + // Map database ids from old properties to new properties + favorites.values.forEach { favorite -> + val oldFavorite = oldFavorites[favorite.userId] + // only update favorite if local is not dirty + if (oldFavorite == null || !oldFavorite.isDirty) { + favorite.observation = observation + observationFavoriteDao.createOrUpdate(favorite) + } + } + + // Remove any favorites that existed in the old observation but do not exist + // in the new observation. + oldFavorites.keys.subtract(favorites.keys).forEach { key -> + // Only delete favorites that are not dirty + if (!oldFavorites[key]!!.isDirty) { + observationFavoriteDao.deleteById(oldFavorites[key]!!.id) + } + } + + Log.i(LOG_NAME, "Observation attachments " + observation.attachments.size) + oldObservation.attachments.forEach { oldAttachment -> + if (oldAttachment.remoteId != null) { + var found: Attachment? = null + observation.attachments.forEach { attachment -> + if (oldAttachment.remoteId == attachment.remoteId) { + found = attachment + attachment.id = oldAttachment.id + } + } + + // if no longer in attachments array response from server, remove it + if (!isServerVersion5(application)) { + if (found == null) { + attachmentLocalDataSource.delete(oldAttachment) + } + } + } + } + + for (attachment in observation.attachments) { + try { + attachment.observation = observation + attachmentLocalDataSource.create(attachment) + } catch (e: Exception) { + throw ObservationException("There was a problem creating/updating the observations attachment: $attachment.", e) + } + } + observationDao.refresh(observation) + if (observation.remoteId != null) { + observation.attachments.filter { it.isDirty }.forEach { attachment -> + attachmentLocalDataSource.uploadableAttachment(attachment) + } + } + observation + } + } catch (e: Exception) { + Log.e(LOG_NAME, "There was a problem updating the observation: $observation.", e) + throw ObservationException("There was a problem updating the observation: $observation.", e) + } + + // fire the event + for (listener in listeners) { + listener.onObservationUpdated(updatedObservation) + } + return updatedObservation + } + + @Throws(ObservationException::class) + fun readAll(): List { + return try { + observationDao.queryForAll() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Observations", e) + throw ObservationException("Unable to read Observations.", e) + } + } + + fun getEventObservations( + event: Event, + filters: List> + ): List { + val query = observationDao.queryBuilder() + val where = query + .orderBy("timestamp", false) + .where() + .eq("event_id", event.id) + + filters.forEach { filter -> + filter.query()?.let { query.join(it) } + filter.and(where) + } + + return observationDao.query(query.prepare()) + } + + /** + * Gets the latest last modified date. Used when fetching. + * + * @return + */ + fun getLatestCleanLastModified(user: User?, event: Event): Date { + var lastModifiedDate = Date(0) + val queryBuilder = observationDao.queryBuilder() + try { + if (user != null) { + queryBuilder.where() + .eq("dirty", java.lang.Boolean.FALSE) + .and() + .ne("user_id", user.remoteId.toString()) + .and() + .eq("event_id", event.id) + queryBuilder.orderBy("last_modified", false) + val o = observationDao.queryForFirst(queryBuilder.prepare()) + if (o != null) { + lastModifiedDate = o.lastModified + } + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not get last_modified date.", e) + } + return lastModifiedDate + } + + /** + * Gets a List of Observations from the datastore that are dirty (i.e. + * should be synced with the server). + * + * @return + */ + val dirty: List + get() { + val queryBuilder = observationDao.queryBuilder() + return try { + queryBuilder.where().eq("dirty", true) + observationDao.query(queryBuilder.prepare()) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not get dirty Observations.", e) + emptyList() + } + } + + /** + * Archive an Observation. This will remove the observation from the server + * + * @param observation + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun archive(observation: Observation) { + if (observation.remoteId == null) { + // observation does not exist on the server yet, just remove it from the database + try { + observationDao.delete(observation) + } catch (e: SQLException) { + throw ObservationException("Unable to archive Observation: " + observation.id, e) + } + } else { + observation.state = State.ARCHIVE + observation.isDirty = true + try { + observationDao.update(observation) + } catch (e: SQLException) { + throw ObservationException("Unable to archive Observation: " + observation.id, e) + } + + for (listener in listeners) { + listener.onObservationUpdated(observation) + } + } + } + + /** + * Deletes an Observation. This will also delete an Observation's child + * Attachments, child Properties and Geometry data. + * + * @param observation + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun delete(observation: Observation) { + try { + observationDao.callBatchTasks { // delete Observation forms. + observation.forms.forEach { form -> + form.properties.forEach { property -> + observationPropertyDao.deleteById(property.id) + + } + + observationFormDao.deleteById(form.id) + } + + // delete Observation favorites. + val favorites = observation.favorites + if (favorites != null) { + for (favorite in favorites) { + observationFavoriteDao.deleteById(favorite.id) + } + } + + // delete Observation attachments. + val attachments = observation.attachments + if (attachments != null) { + for (attachment in attachments) { + attachmentLocalDataSource.delete(attachment) + } + } + + // delete important + val important = observation.important + if (important != null) { + observationImportantDao.deleteById(important.id) + } + + // finally, delete the Observation. + observationDao.deleteById(observation.id) + for (listener in listeners) { + listener.onObservationDeleted(observation) + } + null + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Unable to delete Observation: " + observation.id, e) + throw ObservationException("Unable to delete Observation: " + observation.id, e) + } + } + + /** + * This will delete all observations for an event. + * + * @param event + * The event to remove locations for + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun deleteObservations(event: Event) { + Log.e(LOG_NAME, "Deleting observations for event " + event.name) + try { + val qb = observationDao.queryBuilder() + qb.where().eq("event_id", event.id) + for (observation in qb.query()) { + delete(observation) + } + } catch (sqle: SQLException) { + Log.e(LOG_NAME, "Unable to delete observations for an event", sqle) + throw ObservationException("Unable to delete observations for an event", sqle) + } + } + + /** + * This will mark the observation as important + * + * @param observation The observation to mark as important + * + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun addImportant(observation: Observation) { + val important = observation.important + important!!.isImportant = true + important.isDirty = true + try { + observationImportantDao.createOrUpdate(important) + observationDao.update(observation) + + // fire the event + for (listener in listeners) { + listener.onObservationUpdated(observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to favorite observation", e) + throw ObservationException("Unable to favorite observation", e) + } + } + + /** + * This will remove the important mark from an observation. + * + * @param observation The observation to unfavorite + * + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun removeImportant(observation: Observation) { + try { + observationImportantDao.queryForAll() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Error querying for observations", e) + } + val important = observation.important + if (important != null) { + important.isImportant = false + important.isDirty = true + try { + observationImportantDao.update(important) + observationDao.refresh(observation) + + // fire the event + for (listener in listeners) { + listener.onObservationUpdated(observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to unfavorite observation", e) + throw ObservationException("Unable to unfavorite observation", e) + } + } + } + + @Throws(ObservationException::class) + fun updateImportant(observation: Observation) { + val important = observation.important + try { + if (important!!.isImportant) { + important.isDirty = java.lang.Boolean.FALSE + observation.important = important + observationImportantDao.update(important) + } else { + observationImportantDao.delete(important) + } + + // Update the observation so that the lastModified time is updated + observationDao.update(observation) + observationDao.refresh(observation) + for (listener in listeners) { + listener.onObservationUpdated(observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update observation favorite", e) + throw ObservationException("Unable to update observation favorite", e) + } + } + + /** + * This will favorite and observation for the user. + * + * @param observation The observation to favorite + * @param user The user that is favoriting the observation + * + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun favoriteObservation(observation: Observation, user: User) { + val favoritesMap = observation.favoritesMap + var favorite = favoritesMap[user.remoteId] + if (favorite == null) { + favorite = ObservationFavorite(user.remoteId, true) + } + favorite.observation = observation + favorite.isFavorite = true + favorite.isDirty = true + try { + observationFavoriteDao.createOrUpdate(favorite) + observationDao.refresh(observation) + + // fire the event + for (listener in listeners) { + listener.onObservationUpdated(favorite.observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to favorite observation", e) + throw ObservationException("Unable to favorite observation", e) + } + } + + /** + * This will unfavorite and observation for the user. + * + * @param observation The observation to unfavorite + * @param user The user that is unfavoriting the observation + * + * @throws ObservationException + */ + @Throws(ObservationException::class) + fun unfavoriteObservation(observation: Observation, user: User) { + val favoritesMap = observation.favoritesMap + val favorite = favoritesMap[user.remoteId] + if (favorite != null) { + favorite.isFavorite = false + favorite.isDirty = true + try { + observationFavoriteDao.update(favorite) + observationDao.refresh(observation) + + // fire the event + for (listener in listeners) { + listener.onObservationUpdated(favorite.observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to remove favorite from observation", e) + throw ObservationException("Unable to remove favorite from observation", e) + } + } + } + + @Throws(ObservationException::class) + fun updateFavorite(favorite: ObservationFavorite) { + try { + val observation = favorite.observation + if (favorite.isFavorite) { + favorite.isDirty = java.lang.Boolean.FALSE + observationFavoriteDao.update(favorite) + } else { + observationFavoriteDao.delete(favorite) + } + + // Update the observation so that the lastModified time is updated + observationDao.update(observation) + observationDao.refresh(observation) + for (listener in listeners) { + listener.onObservationUpdated(observation) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update observation favorite", e) + throw ObservationException("Unable to update observation favorite", e) + } + }// TODO Auto-generated catch block + + /** + * A List of [ObservationImportant] from the datastore that are dirty (i.e. + * should be synced with the server). + * + * @return + */ + @get:Throws(ObservationException::class) + val dirtyImportant: List + get() = try { + val importantQb = observationImportantDao.queryBuilder() + importantQb.where().eq("dirty", true) + val observationQb = observationDao.queryBuilder() + observationQb.join(importantQb).query() + } catch (e: SQLException) { + // TODO Auto-generated catch block + Log.e(LOG_NAME, "Unable to get dirty observation favorites", e) + throw ObservationException("Unable to get dirty observation favorites", e) + }// TODO Auto-generated catch block + + /** + * A List of [ObservationProperty] from the datastore that are dirty (i.e. + * should be synced with the server). + * + * @return + */ + @get:Throws(ObservationException::class) + val dirtyFavorites: List + get() = try { + val queryBuilder = observationFavoriteDao.queryBuilder() + queryBuilder.where().eq("dirty", true) + observationFavoriteDao.query(queryBuilder.prepare()) + } catch (e: SQLException) { + // TODO Auto-generated catch block + Log.e(LOG_NAME, "Unable to get dirty observation favorites", e) + throw ObservationException("Unable to get dirty observation favorites", e) + } + + override fun addListener(listener: IObservationEventListener): Boolean { + return listeners.add(listener) + } + + override fun removeListener(listener: IObservationEventListener): Boolean { + return listeners.remove(listener) + } + + companion object { + private val LOG_NAME = ObservationLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/permission/RoleLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/permission/RoleLocalDataSource.kt new file mode 100644 index 000000000..0c9ab548b --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/permission/RoleLocalDataSource.kt @@ -0,0 +1,88 @@ +package mil.nga.giat.mage.data.datasource.permission + +import android.util.Log +import com.j256.ormlite.dao.Dao +import mil.nga.giat.mage.database.model.permission.Role +import mil.nga.giat.mage.sdk.exceptions.RoleException +import java.sql.SQLException +import javax.inject.Inject + +class RoleLocalDataSource @Inject constructor( + private val roleDao: Dao +) { + + @Throws(RoleException::class) + fun create(pRole: Role): Role { + return try { + val createdRole = roleDao.createIfNotExists(pRole) + Log.d(LOG_NAME, "created role with remote_id " + createdRole.remoteId) + createdRole + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating the role: $pRole") + throw RoleException("There was a problem creating the role: $pRole", e) + } + } + + @Throws(RoleException::class) + fun update(pRole: Role): Role { + try { + roleDao.update(pRole) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating role: $pRole") + throw RoleException("There was a problem creating role: $pRole", e) + } + return pRole + } + + fun createOrUpdate(role: Role): Role { + return try { + val oldRole = read(role.remoteId) + if (oldRole == null) { + val newRole = create(role) + Log.d(LOG_NAME, "Created role with remote_id " + role.remoteId) + newRole + } else { + role.id = oldRole.id + update(role) + Log.d(LOG_NAME, "Updated role with remote_id " + role.remoteId) + role + } + } catch (e: RoleException) { + Log.e(LOG_NAME, "There was a problem reading role: $role", e) + role + } + } + + @Throws(RoleException::class) + fun read(id: Long): Role { + return try { + roleDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw RoleException("Unable to query for existence for id = '$id'", e) + } + } + + fun read(pRemoteId: String): Role? { + return try { + val results = roleDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + results.firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", e) + null + } + } + + fun readAll(): Collection { + return try { + roleDao.queryForAll() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to read Observations", e) + emptyList() + } + } + + companion object { + private val LOG_NAME = RoleLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/team/TeamLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/team/TeamLocalDataSource.kt new file mode 100644 index 000000000..b384f4235 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/team/TeamLocalDataSource.kt @@ -0,0 +1,174 @@ +package mil.nga.giat.mage.data.datasource.team + +import android.util.Log +import com.j256.ormlite.dao.Dao +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.database.model.user.UserTeam +import mil.nga.giat.mage.sdk.exceptions.EventException +import mil.nga.giat.mage.sdk.exceptions.TeamException +import java.sql.SQLException +import javax.inject.Inject + +class TeamLocalDataSource @Inject constructor( + private val teamDao: Dao, + private val userTeamDao: Dao, + private val teamEventDao: Dao +) { + @Throws(TeamException::class) + fun create(pTeam: Team): Team { + val createdTeam: Team = try { + teamDao.createIfNotExists(pTeam) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating team: $pTeam", e) + throw TeamException("There was a problem creating team: $pTeam", e) + } + return createdTeam + } + + @Throws(TeamException::class) + fun read(id: Long): Team { + return try { + teamDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw TeamException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(EventException::class) + fun readAll(): MutableList { + val teams: MutableList = ArrayList() + try { + teams.addAll(teamDao.queryForAll()) + } catch (sqle: SQLException) { + Log.e(LOG_NAME, "Unable to read Teams", sqle) + throw EventException("Unable to read Teams.", sqle) + } + return teams + } + + @Throws(TeamException::class) + fun read(pRemoteId: String): Team? { + var team: Team? = null + try { + val results = teamDao.queryBuilder().where().eq("remote_id", pRemoteId).query() + if (results != null && results.size > 0) { + team = results[0] + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$pRemoteId'", e) + throw TeamException("Unable to query for existence for remote_id = '$pRemoteId'", e) + } + return team + } + + @Throws(TeamException::class) + fun update(pTeam: Team): Team { + try { + teamDao.update(pTeam) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating team: $pTeam") + throw TeamException("There was a problem creating team: $pTeam", e) + } + return pTeam + } + + fun createOrUpdate(update: Team): Team? { + return try { + val oldTeam = read(update.remoteId) + if (oldTeam == null) { + val team = create(update) + Log.d(LOG_NAME, "Created team with remote_id " + team.remoteId) + team + } else { + // perform update? + update.id = oldTeam.id + val team = update(update) + Log.d(LOG_NAME, "Updated team with remote_id " + team.remoteId) + team + } + } catch (e: TeamException) { + Log.e(LOG_NAME, "There was a problem reading team: $update", e) + null + } + } + + fun deleteTeamEvents() { + try { + val db = teamEventDao.deleteBuilder() + db.delete() + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem deleting teamevents.", e) + } + } + + fun create(pTeamEvent: TeamEvent): TeamEvent? { + var createdTeamEvent: TeamEvent? = null + try { + createdTeamEvent = teamEventDao.createIfNotExists(pTeamEvent) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating teamevent: $pTeamEvent", e) + } + return createdTeamEvent + } + + fun getTeamsByUser(pUser: User): List { + return try { + val userTeamQuery = userTeamDao.queryBuilder() + userTeamQuery.selectColumns("team_id") + val where = userTeamQuery.where() + where.eq("user_id", pUser.id) + val teamQuery = teamDao.queryBuilder() + teamQuery.where().`in`("_id", userTeamQuery) + teamQuery.query() + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem getting teams for the user: $pUser", e) + emptyList() + } + } + + fun getTeamsByEvent(pEvent: Event): List { + return try { + val teamEventQuery = teamEventDao.queryBuilder() + teamEventQuery.selectColumns("team_id") + val where = teamEventQuery.where() + where.eq("event_id", pEvent.id) + val teamQuery = teamDao.queryBuilder() + teamQuery.where().`in`("_id", teamEventQuery) + teamQuery.query() + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem getting teams for the event: $pEvent", e) + emptyList() + } + } + + /** + * Remove any teams from the database that are not in this team list. + * + * @param remoteTeams list of team that should remain in the database, all others will be removed + */ + fun syncTeams(remoteTeams: Set) { + try { + val teamsToRemove = readAll() + teamsToRemove.removeAll(remoteTeams) + for (teamToRemove in teamsToRemove) { + Log.e(LOG_NAME, "Removing team " + teamToRemove.name) + val teamDeleteBuilder = teamEventDao.deleteBuilder() + teamDeleteBuilder.where().eq("team_id", teamToRemove.id) + teamDeleteBuilder.delete() + val eventDeleteBuilder = teamDao.deleteBuilder() + eventDeleteBuilder.where().idEq(teamToRemove.id) + eventDeleteBuilder.delete() + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error deleting event ", e) + } + } + + companion object { + private val LOG_NAME = TeamLocalDataSource::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/datasource/user/UserLocalDataSource.kt b/mage/src/main/java/mil/nga/giat/mage/data/datasource/user/UserLocalDataSource.kt new file mode 100644 index 000000000..f56d3be29 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/datasource/user/UserLocalDataSource.kt @@ -0,0 +1,327 @@ +package mil.nga.giat.mage.data.datasource.user + +import android.util.Log +import com.j256.ormlite.dao.Dao +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.database.model.user.UserLocal +import mil.nga.giat.mage.database.model.user.UserTeam +import mil.nga.giat.mage.sdk.event.IEventDispatcher +import mil.nga.giat.mage.sdk.event.IEventEventListener +import mil.nga.giat.mage.sdk.event.IUserDispatcher +import mil.nga.giat.mage.sdk.event.IUserEventListener +import mil.nga.giat.mage.sdk.exceptions.UserException +import java.sql.SQLException +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +class UserLocalDataSource @Inject constructor( + private val userDao: Dao, + private val userLocalDao: Dao, + private val userTeamDao: Dao, + private val teamEventDao: Dao, + private val teamLocalDataSource: TeamLocalDataSource +): IEventDispatcher, IUserDispatcher { + + // FIXME : should add user to team if needed + @Throws(UserException::class) + fun create(user: User): User { + try { + val userLocal = userLocalDao.createIfNotExists(UserLocal()) + user.userLocal = userLocal + val newUser = userDao.createIfNotExists(user) + + for (listener in userListeners) { + listener.onUserCreated(newUser) + } + + return newUser + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating user: $user", e) + throw UserException("There was a problem creating user: $user", e) + } + } + + @Throws(UserException::class) + fun read(id: Long): User { + return try { + userDao.queryForId(id) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for id = '$id'", e) + throw UserException("Unable to query for existence for id = '$id'", e) + } + } + + @Throws(UserException::class) + fun read(remoteId: String): User? { + try { + val results = userDao.queryBuilder().where().eq("remote_id", remoteId).query() + return results.firstOrNull() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_id = '$remoteId'", e) + throw UserException("Unable to query for existence for remote_id = '$remoteId'", e) + } + } + + @Throws(UserException::class) + fun read(remoteIds: Collection): List { + return try { + userDao.queryBuilder().where().`in`("remote_id", remoteIds).query() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to query for existence for remote_ids = '$remoteIds'", e) + throw UserException("Unable to query for existence for remote_ids = '$remoteIds'", e) + } + } + + fun readCurrentUser(): User? { + return try { + val userLocalQuery = userLocalDao.queryBuilder() + userLocalQuery.selectColumns(UserLocal.COLUMN_NAME_ID) + val where = userLocalQuery.where() + where.eq(UserLocal.COLUMN_NAME_CURRENT_USER, true) + val userQuery = userDao.queryBuilder() + userQuery.where().`in`(User.COLUMN_NAME_USER_LOCAL_ID, userLocalQuery) + val preparedQuery = userQuery.prepare() + userDao.queryForFirst(preparedQuery) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem reading active users.") + null + } + } + + fun isCurrentUserPartOfCurrentEvent(): Boolean { + return try { + readCurrentUser()?.let { user -> + val currentEvent = user.currentEvent + val userTeams = teamLocalDataSource.getTeamsByUser(user) + val eventTeams = teamLocalDataSource.getTeamsByEvent(currentEvent).toMutableSet() + eventTeams.retainAll(userTeams.toSet()) + eventTeams.size > 0 + } ?: false + } catch (e: Exception) { + Log.e(LOG_NAME, "error determining current user event membership", e) + false + } + } + + @Throws(UserException::class) + fun update(user: User): User { + try { + val oldUser = read(user.id) + user.userLocal = oldUser.userLocal + userDao.update(user) + + for (listener in userListeners) { + listener.onUserUpdated(user) + } + + return user + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating user: $user") + throw UserException("There was a problem creating user: $user", e) + } + } + + fun createOrUpdate(user: User): User { + val db = userDao.queryBuilder() + db.where().eq(User.COLUMN_NAME_USERNAME, user.username) + val oldUser = db.queryForFirst() + + return if (oldUser == null) { + val newUser = create(user) + Log.d(LOG_NAME, "Created user with remote_id " + newUser.remoteId) + newUser + } else { + user.id = oldUser.id + user.userLocal = oldUser.userLocal + userDao.update(user) + Log.d(LOG_NAME, "Updated user with remote_id " + user.remoteId) + + for (listener in userListeners) { + listener.onUserUpdated(user) + } + + user + } + } + + @Throws(UserException::class) + fun setCurrentUser(user: User): User { + try { + clearCurrentUser() + val builder = userLocalDao.updateBuilder() + builder.where().idEq(user.userLocal.id) + builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_USER, true) + builder.update() + userDao.refresh(user) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update user '" + user.displayName + "' to current user", e) + throw UserException("Unable to update UserLocal table", e) + } + return user + } + + @Throws(UserException::class) + fun setCurrentEvent(user: User, event: Event?): User { + try { + val builder = userLocalDao.updateBuilder() + builder.where().idEq(user.userLocal.id) + builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_EVENT, event) + + // check if we need to send event onChange + val userLocal = user.userLocal + if (userLocal.isCurrentUser) { + var oldEventRemoteId: String? = null + if (userLocal.currentEvent != null) { + oldEventRemoteId = userLocal.currentEvent.remoteId + } + val newEventRemoteId = event?.remoteId + + // run update before firing event to make sure update works. + builder.update() + if ((oldEventRemoteId == null) xor (newEventRemoteId == null)) { + for (listener in eventListeners) { + listener.onEventChanged() + } + } else if (oldEventRemoteId != null && newEventRemoteId != null) { + if (oldEventRemoteId != newEventRemoteId) { + for (listener in eventListeners) { + listener.onEventChanged() + } + } + } + userDao.refresh(user) + } + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update users '" + user.displayName + "' current event", e) + throw UserException("Unable to update UserLocal table", e) + } + return user + } + + fun removeCurrentEvent(): User? { + val user = readCurrentUser() ?: return null + + try { + val builder = userLocalDao.updateBuilder() + builder.where().idEq(user.userLocal.id) + builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_EVENT, null) + builder.update() + userDao.refresh(user) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to clear current event for user '" + user.displayName + "'") + } + + return user + } + + @Throws(UserException::class) + fun setAvatarPath(user: User, path: String?): User { + try { + val builder = userLocalDao.updateBuilder() + builder.where().idEq(user.userLocal.id) + builder.updateColumnValue(UserLocal.COLUMN_NAME_AVATAR_PATH, path) + builder.update() + userLocalDao.refresh(user.userLocal) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update users '" + user.displayName + "' avatar path", e) + throw UserException("Unable to update UserLocal table", e) + } + for (listener in userListeners) { + listener.onUserAvatarUpdated(user) + } + return user + } + + @Throws(UserException::class) + fun setIconPath(user: User, path: String?): User { + try { + val builder = userLocalDao.updateBuilder() + builder.where().idEq(user.userLocal.id) + builder.updateColumnValue(UserLocal.COLUMN_NAME_ICON_PATH, path) + builder.update() + userLocalDao.refresh(user.userLocal) + } catch (e: SQLException) { + Log.e(LOG_NAME, "Unable to update users '" + user.displayName + "' icon path", e) + throw UserException("Unable to update UserLocal table", e) + } + for (listener in userListeners) { + listener.onUserIconUpdated(user) + } + return user + } + + @Throws(UserException::class) + private fun clearCurrentUser() { + try { + val builder = userLocalDao.updateBuilder() + builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_USER, java.lang.Boolean.FALSE) + builder.update() + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem deleting active userlocal.", e) + throw UserException("There was a problem deleting active userlocal.", e) + } + } + + fun deleteUserTeams() { + try { + val db = userTeamDao.deleteBuilder() + db.delete() + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem deleting userteams.", e) + } + } + + fun create(userTeam: UserTeam): UserTeam? { + var createdUserTeam: UserTeam? = null + try { + createdUserTeam = userTeamDao.createIfNotExists(userTeam) + } catch (e: SQLException) { + Log.e(LOG_NAME, "There was a problem creating userteam: $userTeam", e) + } + return createdUserTeam + } + + fun getUsersInEvent(event: Event): Collection { + return try { + val teamEventQuery = teamEventDao.queryBuilder() + teamEventQuery.selectColumns("team_id") + val teamEventWhere = teamEventQuery.where() + teamEventWhere.eq("event_id", event.id) + val userTeamQuery = userTeamDao.queryBuilder() + userTeamQuery.selectColumns("user_id") + val userTeamWhere = userTeamQuery.where() + userTeamWhere.`in`("team_id", teamEventQuery) + val teamQuery = userDao.queryBuilder() + teamQuery.where().`in`("_id", userTeamQuery) + teamQuery.query() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Error getting users for event: $event", e) + emptyList() + } + } + + override fun addListener(listener: IEventEventListener): Boolean { + return eventListeners.add(listener) + } + + override fun removeListener(listener: IEventEventListener): Boolean { + return eventListeners.remove(listener) + } + + override fun addListener(listener: IUserEventListener): Boolean { + return userListeners.add(listener) + } + + override fun removeListener(listener: IUserEventListener): Boolean { + return userListeners.add(listener) + } + + companion object { + private val LOG_NAME = UserLocalDataSource::class.java.name + private val userListeners: MutableCollection = CopyOnWriteArrayList() + private val eventListeners: MutableCollection = CopyOnWriteArrayList() + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedRepository.kt deleted file mode 100644 index 1c9fea027..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedRepository.kt +++ /dev/null @@ -1,74 +0,0 @@ -package mil.nga.giat.mage.data.feed - -import android.content.Context -import androidx.annotation.WorkerThread -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import mil.nga.giat.mage.network.Resource -import mil.nga.giat.mage.network.api.FeedService -import mil.nga.giat.mage.network.gson.asLongOrNull -import mil.nga.giat.mage.network.gson.asStringOrNull -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory -import java.text.ParseException -import java.util.* -import javax.inject.Inject - -class FeedRepository @Inject constructor( - @ApplicationContext private val context: Context, - private val feedLocalDao: FeedLocalDao, - private val feedItemDao: FeedItemDao, - private val feedService: FeedService -) { - suspend fun syncFeed(feed: Feed) = withContext(Dispatchers.IO) { - val resource = try { - val event = EventHelper.getInstance(context).currentEvent - - val response = feedService.getFeedItems(event.remoteId, feed.id) - if (response.isSuccessful) { - val content = response.body()!! - saveFeed(feed, content) - Resource.success(content) - } else { - Resource.error(response.message(), null) - } - } catch (e: Exception) { - Resource.error(e.localizedMessage ?: e.toString(), null) - } - - val local = FeedLocal(feed.id) - local.lastSync = Date().time - feedLocalDao.upsert(local) - - resource - } - - @WorkerThread - private fun saveFeed(feed: Feed, content: FeedContent) { - if (!feed.itemsHaveIdentity) { - feedItemDao.removeFeedItems(feed.id) - } - - for (item in content.items) { - item.feedId = feed.id - - item.timestamp = null - if (feed.itemTemporalProperty != null) { - val temporalElement = item.properties?.asJsonObject?.get(feed.itemTemporalProperty) - item.timestamp = temporalElement?.asLongOrNull() ?: run { - temporalElement?.asStringOrNull()?.let { date -> - try { - ISO8601DateFormatFactory.ISO8601().parse(date)?.time - } catch (ignore: ParseException) { null } - } - } - } - - feedItemDao.upsert(item) - } - - val itemIds = content.items.map { it.id } - feedItemDao.preserveFeedItems(feed.id, itemIds) - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedWithItems.kt b/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedWithItems.kt deleted file mode 100644 index 329993f57..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedWithItems.kt +++ /dev/null @@ -1,14 +0,0 @@ -package mil.nga.giat.mage.data.feed - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Relation - -@Entity -data class FeedWithItems( - @Embedded - val feed: Feed, - - @Relation(parentColumn = "id", entityColumn = "feed_id") - val items: List -) \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/ItemWithFeed.kt b/mage/src/main/java/mil/nga/giat/mage/data/feed/ItemWithFeed.kt deleted file mode 100644 index 58e9f5785..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/ItemWithFeed.kt +++ /dev/null @@ -1,14 +0,0 @@ -package mil.nga.giat.mage.data.feed - -import androidx.room.Embedded -import androidx.room.Entity -import androidx.room.Relation - -@Entity -data class ItemWithFeed( - @Relation(parentColumn = "item_feed_id", entityColumn = "id") - val feed: Feed, - - @Embedded(prefix = "item_") - val item: FeedItem -) \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/layer/LayerRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/layer/LayerRepository.kt deleted file mode 100644 index 2cd105647..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/layer/LayerRepository.kt +++ /dev/null @@ -1,49 +0,0 @@ -package mil.nga.giat.mage.data.layer - -import android.app.Application -import android.util.Log -import androidx.preference.PreferenceManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureHelper -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import javax.inject.Inject - -class LayerRepository @Inject constructor( - private val application: Application, -) { - private val layerHelper = LayerHelper.getInstance(application) - private val eventHelper = EventHelper.getInstance(application) - private val staticFeatureHelper = StaticFeatureHelper.getInstance(application) - - suspend fun getStaticFeatureLayers(eventId: Long) = withContext(Dispatchers.IO) { - val preferences = PreferenceManager.getDefaultSharedPreferences(application) - val enabledLayers = preferences.getStringSet(application.getString(R.string.tileOverlaysKey), null) ?: emptySet() - - try { - layerHelper.readByEvent(eventHelper.read(eventId), "Feature") - .asSequence() - .filter { it.isLoaded } - .filter { enabledLayers.contains(it.name) } - .toList() - } catch (e: Exception) { - Log.w(LOG_NAME, "Failed to load feature layers", e) - emptyList() - } - } - - suspend fun getStaticFeature(layerId: Long, featureId: Long): StaticFeature = withContext(Dispatchers.IO) { - staticFeatureHelper.readFeature(layerId, featureId) - } - - suspend fun getStaticFeatures(layerId: Long): Collection = withContext(Dispatchers.IO) { - layerHelper.read(layerId).staticFeatures - } - - companion object { - private val LOG_NAME = LayerRepository::class.java.simpleName - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/repository/api/ApiRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/api/ApiRepository.kt new file mode 100644 index 000000000..a433120ed --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/api/ApiRepository.kt @@ -0,0 +1,139 @@ +package mil.nga.giat.mage.data.repository.api + +import android.app.Application +import android.content.SharedPreferences +import android.util.Log +import mil.nga.giat.mage.R +import mil.nga.giat.mage.network.api.ApiService +import mil.nga.giat.mage.sdk.Compatibility +import org.json.JSONException +import org.json.JSONObject +import javax.inject.Inject + +sealed class ApiResponse { + object Valid: ApiResponse() + object Invalid: ApiResponse() + data class Error(val message: String): ApiResponse() +} + +class ApiRepository @Inject constructor( + private val application: Application, + private val apiService: ApiService, + private val preferences: SharedPreferences +) { + suspend fun getApi(url: String): ApiResponse { + return try { + val response = apiService.getApi("$url/api") + if (response.isSuccessful) { + val json = response.body()?.string() ?: "{}" + val apiJson = JSONObject(json) + removeValues() + populateValues(SERVER_API_PREFERENCE_PREFIX, apiJson) + parseAuthenticationStrategies(apiJson) + if (isApiValid()) ApiResponse.Valid else ApiResponse.Invalid + } else { + val message = response.errorBody()?.string() ?: "Application is not compatible with server" + ApiResponse.Error(message) + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error fetching API", e) + ApiResponse.Error(e.cause?.localizedMessage ?: "Cannot connect to server.") + } + } + + private fun isApiValid(): Boolean { + // check versions + var majorVersion: Int? = null + if (preferences.contains(application.getString(R.string.serverVersionMajorKey))) { + majorVersion = preferences.getInt(application.getString(R.string.serverVersionMajorKey), 0) + } + var minorVersion: Int? = null + if (preferences.contains(application.getString(R.string.serverVersionMinorKey))) { + minorVersion = preferences.getInt(application.getString(R.string.serverVersionMinorKey), 0) + } + return Compatibility.isCompatibleWith(majorVersion!!, minorVersion!!) + } + + private fun populateValues(sharedPreferenceName: String, json: JSONObject) { + val iterator = json.keys() + while (iterator.hasNext()) { + val key = iterator.next() + try { + val value = json[key] + if (value is JSONObject) { + populateValues( + sharedPreferenceName + key[0].uppercaseChar() + if (key.length > 1) key.substring( + 1 + ) else "", value + ) + } else { + val editor = preferences.edit() + val keyString = sharedPreferenceName + key[0].uppercaseChar() + if (key.length > 1) key.substring(1) else "" + Log.i(LOG_NAME, keyString + " is " + preferences.all[keyString] + ". Setting it to " + value + ".") + when (value) { + is Number -> { + when (value) { + is Long -> editor.putLong(keyString, value) + is Float -> editor.putFloat(keyString, value) + is Double -> editor.putFloat(keyString, value.toFloat()) + is Int -> editor.putInt(keyString, value) + is Short -> editor.putInt(keyString, value.toInt()) + else -> { + Log.e(LOG_NAME, "$keyString with value $value is not of valid number type. Skipping this key-value pair.") + } + } + } + is Boolean -> editor.putBoolean(keyString, value) + is String -> editor.putString(keyString, value) + is Char -> editor.putString(keyString, Character.toString(value)) + else -> { + try { + editor.putString(keyString, value.toString()) + } catch (e: Exception) { + Log.e(LOG_NAME, "$keyString with value $value is not of valid type. Skipping this key-value pair.") + } + } + } + + editor.apply() + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error parsing api json", e) + } + } + } + + private fun removeValues() { + val editor = preferences.edit() + for (key in preferences.all.keys) { + if (key.matches(SERVER_API_PREFERENCE_PREFIX_REGEX)) { + editor.remove(key) + } + } + editor.apply() + } + + private fun parseAuthenticationStrategies(json: JSONObject) { + try { + val value = json[SERVER_API_AUTHENTICATION_STRATEGIES_KEY] + if (value is JSONObject) { + val editor = preferences.edit() + editor.putString( + application.resources.getString(R.string.authenticationStrategiesKey), + value.toString() + ) + editor.apply() + } + } catch (e: JSONException) { + Log.e(LOG_NAME, "Error parsing server api json", e) + } + } + + companion object { + private val LOG_NAME = ApiRepository::class.java.name + + private val SERVER_API_PREFERENCE_PREFIX_REGEX = Regex("^g[A-Z]\\w*") + private const val SERVER_API_PREFERENCE_PREFIX = "g" + private const val SERVER_API_AUTHENTICATION_STRATEGIES_KEY = "authenticationStrategies" + } +} diff --git a/mage/src/main/java/mil/nga/giat/mage/data/event/EventRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/event/EventRepository.kt similarity index 65% rename from mage/src/main/java/mil/nga/giat/mage/data/event/EventRepository.kt rename to mage/src/main/java/mil/nga/giat/mage/data/repository/event/EventRepository.kt index 854c79695..0d2ccb769 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/event/EventRepository.kt +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/event/EventRepository.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.event +package mil.nga.giat.mage.data.repository.event import android.content.Context import android.os.Environment @@ -10,23 +10,36 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mil.nga.geopackage.GeoPackageFactory -import mil.nga.giat.mage.data.feed.FeedDao -import mil.nga.giat.mage.data.user.UserRepository +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.data.datasource.permission.RoleLocalDataSource +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.database.model.user.UserTeam import mil.nga.giat.mage.glide.GlideApp import mil.nga.giat.mage.glide.model.Avatar import mil.nga.giat.mage.map.preference.MapLayerPreferences import mil.nga.giat.mage.network.Resource -import mil.nga.giat.mage.network.api.* -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper -import mil.nga.giat.mage.sdk.datastore.user.* +import mil.nga.giat.mage.network.event.EventService +import mil.nga.giat.mage.network.feed.FeedService +import mil.nga.giat.mage.network.layer.LayerService +import mil.nga.giat.mage.network.observation.ObservationService +import mil.nga.giat.mage.network.role.RoleService +import mil.nga.giat.mage.network.team.TeamService import mil.nga.giat.mage.sdk.utils.ZipUtility import java.io.File -import java.util.* +import java.util.Date import javax.inject.Inject class EventRepository @Inject constructor( @ApplicationContext private val context: Context, private val mapLayerPreferences: MapLayerPreferences, + private val teamLocalDataSource: TeamLocalDataSource, private val feedDao: FeedDao, private val roleService: RoleService, private val feedService: FeedService, @@ -34,13 +47,13 @@ class EventRepository @Inject constructor( private val layerService: LayerService, private val eventService: EventService, private val observationService: ObservationService, - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val roleLocalDataSource: RoleLocalDataSource, + private val userLocalDataSource: UserLocalDataSource, + private val layerLocalDataSource: LayerLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource ) { - private val roleHelper = RoleHelper.getInstance(context) - private val teamHelper = TeamHelper.getInstance(context) - private val eventHelper = EventHelper.getInstance(context) - suspend fun getEvents(forceUpdate: Boolean): List { return withContext(Dispatchers.IO) { if (forceUpdate) { @@ -52,7 +65,7 @@ class EventRepository @Inject constructor( } } - eventHelper.readAll() + eventLocalDataSource.readAll() } } @@ -81,30 +94,28 @@ class EventRepository @Inject constructor( private suspend fun syncRoles() { val response = roleService.getRoles() if (response.isSuccessful) { - val roles = response.body()!! - for (role in roles) { - roleHelper.createOrUpdate(role) - } + val roles = response.body() ?: emptyList() + roles.forEach { roleLocalDataSource.createOrUpdate(it) } } } private suspend fun fetchEvents(): List { - teamHelper.deleteTeamEvents() + teamLocalDataSource.deleteTeamEvents() val response = eventService.getEvents() return if (response.isSuccessful) { - val events = response.body()!! - for (event in events.keys) { + val events = response.body() ?: emptyList() + events.forEach { event -> try { - eventHelper.createOrUpdate(event) - } catch (e: java.lang.Exception) { + eventLocalDataSource.createOrUpdate(event) + } catch (e: Exception) { Log.e(LOG_NAME, "Error saving event to database", e) } } - eventHelper.syncEvents(events.keys) + eventLocalDataSource.syncEvents(events) - events.keys.toList() + events } else emptyList() } @@ -113,39 +124,49 @@ class EventRepository @Inject constructor( val response = teamService.getTeams(event.remoteId) if (response.isSuccessful) { - val teams = response.body()!! + val teams = response.body() ?: emptyMap() Log.d(LOG_NAME, "Fetched " + teams.size + " teams") - val userHelper = UserHelper.getInstance(context) - userHelper.deleteUserTeams() + userLocalDataSource.deleteUserTeams() - val teamHelper = TeamHelper.getInstance(context) - for (team in teams.keys) { - val updatedTeam = teamHelper.createOrUpdate(team) - val users = teams[updatedTeam]!! - for (user in users) { + teams.keys.forEach { team -> + val updatedTeam = teamLocalDataSource.createOrUpdate(team) + teams[updatedTeam]?.forEach { (user, roleId) -> user.fetchedDate = Date() - var updatedUser = userHelper.createOrUpdate(user) + user.role = roleLocalDataSource.read(roleId) + userLocalDataSource.read(user.remoteId)?.let { localUser -> + user.id = localUser.id + } + + val updatedUser = userLocalDataSource.createOrUpdate(user) if (updatedUser.avatarUrl != null) { GlideApp.with(context) .download(Avatar.forUser(updatedUser)) .submit(MAX_AVATAR_DIMENSION, MAX_AVATAR_DIMENSION) } + if (updatedUser.iconUrl != null) { iconUsers.add(updatedUser) } - if (userHelper.read(updatedUser.remoteId) == null) { - updatedUser = userHelper.createOrUpdate(updatedUser) - } + + val teamUser = if (userLocalDataSource.read(updatedUser.remoteId) == null) { + userLocalDataSource.createOrUpdate(updatedUser) + } else updatedUser + // populate the user/team join table - userHelper.create(UserTeam(updatedUser, updatedTeam)) + userLocalDataSource.create(UserTeam(teamUser, updatedTeam)) } // populate the team/event join table - teamHelper.create(TeamEvent(updatedTeam, event)) + teamLocalDataSource.create( + TeamEvent( + updatedTeam, + event + ) + ) } - TeamHelper.getInstance(context).syncTeams(teams.keys) + teamLocalDataSource.syncTeams(teams.keys) } return iconUsers @@ -187,12 +208,11 @@ class EventRepository @Inject constructor( private suspend fun syncLayers(event: Event) { val response = layerService.getLayers(event.remoteId, "GeoPackage") if (response.isSuccessful) { - val layers = response.body()!! + val layers = response.body() ?: emptyList() - val layerHelper = LayerHelper.getInstance(context) - layerHelper.deleteAll("GeoPackage") + layerLocalDataSource.deleteAll("GeoPackage") val manager = GeoPackageFactory.getManager(context) - for (layer in layers) { // Check if geopackage has been downloaded as part of another event + layers.forEach { layer -> // Check if geopackage has been downloaded as part of another event val relativePath = String.format("MAGE/geopackages/%s/%s", layer.remoteId, layer.fileName) val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), relativePath) if (file.exists() && manager.existsAtExternalFile(file)) { @@ -200,7 +220,7 @@ class EventRepository @Inject constructor( layer.relativePath = relativePath } layer.event = event - layerHelper.create(layer) + layerLocalDataSource.create(layer) } } } @@ -210,8 +230,8 @@ class EventRepository @Inject constructor( val response = feedService.getFeeds(event.remoteId) if (response.isSuccessful) { var enabledFeeds = mapLayerPreferences.getEnabledFeeds(event.id).toMutableSet() - val feeds = response.body()!! - for (feed in feeds) { + val feeds = response.body() ?: emptyList() + feeds.forEach { feed -> feed.eventRemoteId = event.remoteId val upserted = feedDao.upsert(feed) if (upserted && feed.itemsHaveSpatialDimension) { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/repository/feed/FeedRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/feed/FeedRepository.kt new file mode 100644 index 000000000..e1ba7173a --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/feed/FeedRepository.kt @@ -0,0 +1,78 @@ +package mil.nga.giat.mage.data.repository.feed + +import androidx.annotation.WorkerThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedContent +import mil.nga.giat.mage.database.dao.feed.FeedItemDao +import mil.nga.giat.mage.database.model.feed.FeedLocal +import mil.nga.giat.mage.database.dao.feed.FeedLocalDao +import mil.nga.giat.mage.network.Resource +import mil.nga.giat.mage.network.feed.FeedService +import mil.nga.giat.mage.network.gson.asLongOrNull +import mil.nga.giat.mage.network.gson.asStringOrNull +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import java.text.ParseException +import java.util.* +import javax.inject.Inject + +class FeedRepository @Inject constructor( + private val feedLocalDao: FeedLocalDao, + private val feedItemDao: FeedItemDao, + private val feedService: FeedService, + private val eventLocalDataSource: EventLocalDataSource +) { + suspend fun syncFeed(feed: Feed) = withContext(Dispatchers.IO) { + val resource = try { + eventLocalDataSource.currentEvent?.let { event -> + val response = feedService.getFeedItems(event.remoteId, feed.id) + if (response.isSuccessful) { + response.body()?.let { content -> + saveFeed(feed, content) + Resource.success(content) + } ?: Resource.error("Error parsing feed content body", null) + } else { + Resource.error(response.message(), null) + } + } + } catch (e: Exception) { + Resource.error(e.localizedMessage ?: e.toString(), null) + } + + val local = FeedLocal(feed.id) + local.lastSync = Date().time + feedLocalDao.upsert(local) + + resource + } + + @WorkerThread + private fun saveFeed(feed: Feed, content: FeedContent) { + if (!feed.itemsHaveIdentity) { + feedItemDao.removeFeedItems(feed.id) + } + + for (item in content.items) { + item.feedId = feed.id + + item.timestamp = null + if (feed.itemTemporalProperty != null) { + val temporalElement = item.properties?.asJsonObject?.get(feed.itemTemporalProperty) + item.timestamp = temporalElement?.asLongOrNull() ?: run { + temporalElement?.asStringOrNull()?.let { date -> + try { + ISO8601DateFormatFactory.ISO8601().parse(date)?.time + } catch (ignore: ParseException) { null } + } + } + } + + feedItemDao.upsert(item) + } + + val itemIds = content.items.map { it.id } + feedItemDao.preserveFeedItems(feed.id, itemIds) + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/repository/layer/LayerRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/layer/LayerRepository.kt new file mode 100644 index 000000000..1f23e3c66 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/layer/LayerRepository.kt @@ -0,0 +1,271 @@ +package mil.nga.giat.mage.data.repository.layer + +import android.app.Application +import android.util.Log +import androidx.preference.PreferenceManager +import com.google.common.io.ByteStreams +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mil.nga.giat.mage.R +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.network.layer.LayerService +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.data.datasource.feature.FeatureLocalDataSource +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.sdk.exceptions.StaticFeatureException +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.URL +import javax.inject.Inject + +class LayerRepository @Inject constructor( + private val application: Application, + private val layerService: LayerService, + private val layerLocalDataSource: LayerLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val featureLocalDataSource: FeatureLocalDataSource +) { + + @Throws(IOException::class) + suspend fun fetchLayers(event: Event, type: String): List { + var layers = emptyList() + val response = layerService.getLayers(event.remoteId, type) + if (response.isSuccessful) { + layers = response.body() ?: emptyList() + } else { + Log.e(LOG_NAME, "Error fetching layers") + response.errorBody()?.let { body -> + Log.e(LOG_NAME, body.string()) + } + } + + return layers + } + + suspend fun getStaticFeatureLayers(eventId: Long) = withContext(Dispatchers.IO) { + val preferences = PreferenceManager.getDefaultSharedPreferences(application) + val enabledLayers = preferences.getStringSet(application.getString(R.string.tileOverlaysKey), null) ?: emptySet() + + layerLocalDataSource.readByEvent(eventLocalDataSource.read(eventId), "Feature") + .asSequence() + .filter { it.isLoaded } + .filter { enabledLayers.contains(it.name) } + .toList() + } + + suspend fun getStaticFeature(layerId: Long, featureId: Long): StaticFeature? = withContext(Dispatchers.IO) { + featureLocalDataSource.readFeature(layerId, featureId) + } + + suspend fun getStaticFeatures(layerId: Long): Collection = withContext(Dispatchers.IO) { + layerLocalDataSource.read(layerId).staticFeatures + } + + @Throws(IOException::class) + suspend fun fetchFeatureIcon(url: String): InputStream? { + var inputStream: InputStream? = null + + val response = layerService.getFeatureIcon(url) + if (response.isSuccessful) { + inputStream = response.body()?.byteStream() + } else { + Log.e(LOG_NAME, "Error fetching feature icon") + response.errorBody()?.let { body -> + Log.e(LOG_NAME, body.string()) + } + } + + return inputStream + } + + suspend fun fetchImageryLayers() { + val event = eventLocalDataSource.currentEvent ?: return + + try { + val response = layerService.getLayers(event.remoteId, "Imagery") + if (response.isSuccessful) { + val remoteLayers = response.body() ?: emptyList() + + val localLayers: List = layerLocalDataSource.readAll("Imagery") + val remoteIdToLayer: MutableMap = HashMap(localLayers.size) + val it: Iterator = localLayers.iterator() + while (it.hasNext()) { + val localLayer = it.next() + + // Delete layer not returned from server + if (!remoteLayers.contains(localLayer)) { +// it.remove() + layerLocalDataSource.delete(localLayer.id) + } else { + remoteIdToLayer[localLayer.remoteId] = localLayer + } + } + + remoteLayers.forEach { remoteLayer -> + remoteLayer.event = event + remoteLayer.isLoaded = true + if (!localLayers.contains(remoteLayer)) { + layerLocalDataSource.create(remoteLayer) + } else { + remoteIdToLayer[remoteLayer.remoteId]?.let { localLayer -> + layerLocalDataSource.delete(localLayer.id) + layerLocalDataSource.create(remoteLayer) + } + } + } + } else { + Log.w(LOG_NAME, "Error fetching imagery layers") + } + } catch (e: Exception) { + Log.w(LOG_NAME, "Error performing imagery layer operations", e) + } + } + + suspend fun fetchFeatureLayers(event: Event, deleteLocal: Boolean): List { + val newLayers = mutableListOf() + + Log.d(LOG_NAME, "Pulling static layers for event " + event.name) + + try { + if (deleteLocal) { + layerLocalDataSource.deleteAll("Feature") + } + + val response = layerService.getLayers(event.remoteId, "Feature") + if (response.isSuccessful) { + val remoteLayers = response.body() ?: emptyList() + + // get local layers + val localLayers = layerLocalDataSource.readAll("Feature") + val remoteIdToLayer: MutableMap = java.util.HashMap(localLayers.size) + val it = localLayers.iterator() + while (it.hasNext()) { + val localLayer = it.next() + + // See if the layer has been deleted on the server + if (!remoteLayers.contains(localLayer)) { +// it.remove() + layerLocalDataSource.delete(localLayer.id) + } else { + remoteIdToLayer[localLayer.remoteId] = localLayer + } + } + + for (remoteLayer in remoteLayers) { + remoteLayer.event = event + if (!localLayers.contains(remoteLayer)) { + layerLocalDataSource.create(remoteLayer) + } else { + val localLayer = remoteIdToLayer[remoteLayer.remoteId] + if (remoteLayer.event != localLayer!!.event) { + layerLocalDataSource.delete(localLayer.id) + layerLocalDataSource.create(remoteLayer) + } + } + } + newLayers.addAll(layerLocalDataSource.readAll("Feature")) + } else { + Log.e(LOG_NAME, "Error fetching static layers") + } + } catch (e: java.lang.Exception) { + Log.e(LOG_NAME, "Problem creating layers.", e) + } + + return newLayers + } + + suspend fun loadFeatures(layer: Layer) { + + try { + if (!layer.isLoaded) { + layer.downloadId = 1L + layerLocalDataSource.update(layer) + + Log.i(LOG_NAME, "Loading static features for layer " + layer.name + ".") + val features = fetchFeatures(layer) + + // Pull down the icons + val failedIconUrls = mutableListOf() + features + .mapNotNull { feature -> + feature.propertiesMap["styleiconstyleiconhref"]?.let { url -> + feature to url.key + } + } + .forEach { (feature, url) -> + if (url != null) { + var iconFile: File? = null + try { + val iconUrl = URL(url) + var filename = iconUrl.file + // remove leading / + if (filename != null) { + filename = filename.trim { it <= ' ' } + while (filename!!.startsWith("/")) { + filename = filename.substring(1) + } + } + iconFile = File(application.filesDir.toString() + "/icons/staticfeatures", filename) + if (!iconFile.exists()) { + iconFile.parentFile?.mkdirs() + iconFile.createNewFile() + val inputStream = fetchFeatureIcon(url) + if (inputStream != null) { + ByteStreams.copy(inputStream, FileOutputStream(iconFile)) + feature.localPath = iconFile.absolutePath + } + } else { + feature.localPath = iconFile.absolutePath + } + } catch (e: java.lang.Exception) { + // this block should never flow exceptions up! Log for now. + Log.w(LOG_NAME, "Could not get icon.", e) + failedIconUrls.add(url) + if (iconFile != null && iconFile.exists()) { + iconFile.delete() + } + } + } + } + + val updatedLayer = featureLocalDataSource.createAll(features, layer) + try { + updatedLayer.isLoaded = true + updatedLayer.downloadId = null + layerLocalDataSource.update(updatedLayer) + } catch (e: java.lang.Exception) { + throw StaticFeatureException("Unable to update the layer to loaded: " + layer.name) + } + Log.i(LOG_NAME, "Loaded static features for layer " + layer.name) + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Problem loading layers.", e) + } + } + + private suspend fun fetchFeatures(layer: Layer): List { + var features = emptyList() + + val response = layerService.getFeatures(layer.event.remoteId, layer.remoteId) + if (response.isSuccessful) { + features = response.body() ?: emptyList() + } else { + Log.e(LOG_NAME, "Error fetching static features") + if (response.errorBody() != null) { + response.errorBody()?.let { body -> + Log.e(LOG_NAME, body.string()) + } + } + } + + return features + } + + companion object { + private val LOG_NAME = LayerRepository::class.java.simpleName + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/location/LocationRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/location/LocationRepository.kt similarity index 60% rename from mage/src/main/java/mil/nga/giat/mage/data/location/LocationRepository.kt rename to mage/src/main/java/mil/nga/giat/mage/data/repository/location/LocationRepository.kt index 34191235b..0590bb061 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/location/LocationRepository.kt +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/location/LocationRepository.kt @@ -1,15 +1,11 @@ -package mil.nga.giat.mage.data.location +package mil.nga.giat.mage.data.repository.location -import android.Manifest import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences -import android.content.pm.PackageManager import android.os.BatteryManager import android.util.Log -import androidx.core.content.ContextCompat -import com.j256.ormlite.stmt.Where import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.* import kotlinx.coroutines.channels.ProducerScope @@ -18,25 +14,22 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.flowOn import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.di.TokenProvider import mil.nga.giat.mage.filter.DateTimeFilter import mil.nga.giat.mage.filter.Filter import mil.nga.giat.mage.location.LocationAccess -import mil.nga.giat.mage.network.api.LocationService +import mil.nga.giat.mage.network.location.LocationService import mil.nga.giat.mage.sdk.Temporal -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.Permission -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.database.model.location.LocationProperty +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.event.ILocationEventListener import mil.nga.giat.mage.sdk.exceptions.LocationException import mil.nga.giat.mage.sdk.exceptions.UserException -import mil.nga.giat.mage.sdk.fetch.UserServerFetch -import mil.nga.giat.mage.sdk.http.resource.LocationResource -import mil.nga.giat.mage.sdk.utils.UserUtility import mil.nga.sf.Point import java.sql.SQLException import java.util.* @@ -46,12 +39,13 @@ class LocationRepository @Inject constructor( @ApplicationContext private val context: Context, private val preferences: SharedPreferences, private val locationAccess: LocationAccess, - private val locationService: LocationService + private val locationService: LocationService, + private val userRepository: UserRepository, + private val tokenProvider: TokenProvider, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val locationLocalDataSource: LocationLocalDataSource ) { - private val userFetch: UserServerFetch = UserServerFetch(context) - private val userHelper: UserHelper = UserHelper.getInstance(context) - private val locationHelper: LocationHelper = LocationHelper.getInstance(context) - private val locationDao = DaoStore.getInstance(context).locationDao private var batteryStatus: Intent? = context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) private var refreshTime: Long = 0 @@ -64,23 +58,56 @@ class LocationRepository @Inject constructor( if (gpsLocation.time > 0) { val locationProperties = ArrayList() - val locationHelper = LocationHelper.getInstance(context) - - locationProperties.add(LocationProperty("accuracy", gpsLocation.accuracy)) - locationProperties.add(LocationProperty("bearing", gpsLocation.bearing)) - locationProperties.add(LocationProperty("speed", gpsLocation.speed)) - locationProperties.add(LocationProperty("provider", gpsLocation.provider)) - locationProperties.add(LocationProperty("altitude", gpsLocation.altitude)) - locationProperties.add(LocationProperty("accuracy_type", if (locationAccess.isPreciseLocationGranted()) "PRECISE" else "COARSE")) + locationProperties.add( + LocationProperty( + "accuracy", + gpsLocation.accuracy + ) + ) + locationProperties.add( + LocationProperty( + "bearing", + gpsLocation.bearing + ) + ) + locationProperties.add( + LocationProperty( + "speed", + gpsLocation.speed + ) + ) + locationProperties.add( + LocationProperty( + "provider", + gpsLocation.provider + ) + ) + locationProperties.add( + LocationProperty( + "altitude", + gpsLocation.altitude + ) + ) + locationProperties.add( + LocationProperty( + "accuracy_type", + if (locationAccess.isPreciseLocationGranted()) "PRECISE" else "COARSE" + ) + ) val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) level?.let { - locationProperties.add(LocationProperty("battery_level", it)) + locationProperties.add( + LocationProperty( + "battery_level", + it + ) + ) } var user: User? = null try { - user = UserHelper.getInstance(context).readCurrentUser() + user = userLocalDataSource.readCurrentUser() } catch (e: UserException) { Log.e(LOG_NAME, "Error reading current user from database", e) } @@ -93,9 +120,10 @@ class LocationRepository @Inject constructor( locationProperties, Point(gpsLocation.longitude, gpsLocation.latitude), Date(gpsLocation.time), - user.currentEvent) + user.currentEvent + ) - locationHelper.create(location) + locationLocalDataSource.create(location) } catch (e: LocationException) { Log.e(LOG_NAME, "Error saving GPS location", e) } @@ -106,62 +134,47 @@ class LocationRepository @Inject constructor( } suspend fun pushLocations(): Boolean = withContext(Dispatchers.IO) { - if (UserUtility.getInstance(context).isTokenExpired) { + if (tokenProvider.isExpired()) { return@withContext false } - val locationResource = LocationResource(context) - val locationHelper = LocationHelper.getInstance(context) - var currentUser: User? = null - try { - currentUser = UserHelper.getInstance(context).readCurrentUser() - } catch (e: UserException) { - Log.e(LOG_NAME, "error reading current user", e) - } + val currentUser = userLocalDataSource.readCurrentUser() ?: return@withContext false var success = true - var locations = locationHelper.getCurrentUserLocations(LOCATION_PUSH_BATCH_SIZE, false) + var locations = locationLocalDataSource.getCurrentUserLocations(currentUser, LOCATION_PUSH_BATCH_SIZE, false) // TODO: when locations can't be pushed, this condition never becomes false to terminate the loop while (locations.isNotEmpty()) { // Send locations for the current event val event = locations[0].event - - val eventLocations = ArrayList() - for (l in locations) { - if (event == l.event) { - eventLocations.add(l) - } - } + val eventLocations = locations.filter { it.event == event } try { - if (locationResource.createLocations(event, eventLocations)) { + val response = locationService.pushLocations(event.remoteId, eventLocations) + if (response.isSuccessful) { + val pushedLocations = response.body() ?: emptyList() // We've sync-ed locations to the server, lets remove the locations we synced from the database - Log.d(LOG_NAME, "Pushed " + eventLocations.size + " locations.") - - // Delete location where: - // * user is current user - // * remote id is set. (have been sent to server) - // * past the lower n amount + Log.d(LOG_NAME, "Pushed " + pushedLocations.size + " locations.") try { - if (currentUser != null) { - val locationDao = DaoStore.getInstance(context).locationDao - val queryBuilder = locationDao.queryBuilder() - val where = queryBuilder.where().eq("user_id", currentUser.id) - where.and().isNotNull("remote_id").and().eq("event_id", event.id) - queryBuilder.orderBy("timestamp", false) - val pushedLocations = queryBuilder.query() - - if (pushedLocations.size > minNumberOfLocationsToKeep) { - val locationsToDelete = pushedLocations.subList( - minNumberOfLocationsToKeep, pushedLocations.size) - - try { - LocationHelper.getInstance(context).delete(locationsToDelete) - } catch (e: LocationException) { - Log.e(LOG_NAME, "Could not delete locations.", e) - } + eventLocations.forEachIndexed { index, location -> + val remoteId = pushedLocations.getOrNull(index)?.remoteId + if (remoteId == null) { + locationLocalDataSource.delete(listOf(location)) + } else { + location.remoteId = remoteId + locationLocalDataSource.update(location) + } + } + val syncedLocations = locationLocalDataSource.getSyncedLocations(currentUser, event) + + if (syncedLocations.size > minNumberOfLocationsToKeep) { + val locationsToDelete = syncedLocations.subList(minNumberOfLocationsToKeep, syncedLocations.size) + + try { + locationLocalDataSource.delete(locationsToDelete) + } catch (e: LocationException) { + Log.e(LOG_NAME, "Could not delete locations.", e) } } } catch (e: SQLException) { @@ -169,10 +182,13 @@ class LocationRepository @Inject constructor( } } else { Log.e(LOG_NAME, "Failed to push locations.") + response.errorBody()?.string()?.let { + Log.e(LOG_NAME, "Failed to push locations with error $it") + } success = false } - locations = locationHelper.getCurrentUserLocations(LOCATION_PUSH_BATCH_SIZE, false) + locations = locationLocalDataSource.getCurrentUserLocations(currentUser, LOCATION_PUSH_BATCH_SIZE, false) } catch (e: Exception) { Log.e(LOG_NAME, "Failed to push user locations to the server", e) } @@ -180,6 +196,7 @@ class LocationRepository @Inject constructor( success } + @OptIn(ExperimentalCoroutinesApi::class) fun getLocations(): Flow> = callbackFlow { val locationListener = object: ILocationEventListener { override fun onLocationCreated(locations: Collection) { @@ -193,7 +210,7 @@ class LocationRepository @Inject constructor( override fun onLocationDeleted(location: MutableCollection) {} override fun onError(error: Throwable?) {} } - locationHelper.addListener(locationListener) + locationLocalDataSource.addListener(locationListener) val locationFilterKey = context.resources.getString(R.string.activeLocationTimeFilterKey) val preferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> @@ -206,42 +223,16 @@ class LocationRepository @Inject constructor( trySend(query(this)) awaitClose { - locationHelper.removeListener(locationListener) + locationLocalDataSource.removeListener(locationListener) preferences.unregisterOnSharedPreferenceChangeListener(preferencesListener) } }.flowOn(Dispatchers.IO) + @OptIn(ExperimentalCoroutinesApi::class) private fun query(scope: ProducerScope>): List { - val dao = DaoStore.getInstance(context).locationDao - val query = dao.queryBuilder() - val where: Where = query.where() - - val currentUser: User? = try { - userHelper.readCurrentUser() - } catch (ignore: UserException) { null } - - if (currentUser != null) { - where - .ne("user_id", currentUser.id) - .and() - .eq("event_id", currentUser.userLocal.currentEvent.id) - } - - getTemporalFilter()?.let { filter -> - filter.query()?.let { query.join(it) } - filter.and(where) - } - - query.orderBy("timestamp", false) - - val iterator = locationDao.iterator(query.prepare()) - - val locations = mutableListOf() - while(iterator.hasNext()) { - locations.add(iterator.current()) - } - + val user = userLocalDataSource.readCurrentUser() + val locations = locationLocalDataSource.getAllUsersLocations(user, getTemporalFilter()) locations.lastOrNull()?.let { location -> if (oldestLocation == null || oldestLocation?.timestamp?.after(location.timestamp) == true) { oldestLocation = location @@ -320,24 +311,18 @@ class LocationRepository @Inject constructor( } suspend fun fetch() = withContext(Dispatchers.IO) { - var currentUser: User? = null - try { - currentUser = userHelper.readCurrentUser() - } catch (e: UserException) { - Log.e(LOG_NAME, "Error reading current user.", e) - } + val currentEvent = eventLocalDataSource.currentEvent ?: return@withContext + val currentUser = userLocalDataSource.readCurrentUser() ?: return@withContext - val event = EventHelper.getInstance(context).currentEvent try { - val response = locationService.getLocations(event.remoteId) + val response = locationService.getLocations(currentEvent.remoteId) if (response.isSuccessful) { - val foo = response.body()!! - val locations = foo.map { - it.event = event - it - } + val locations = response.body()?.flatMap { (_, locations) -> + locations.forEach { it.event = currentEvent } + locations + } ?: emptyList() - for (location in locations) { + locations.forEach { location -> // make sure that the user exists and is persisted in the local data-store var userId: String? = null val userIdProperty = location.propertiesMap["userId"] @@ -345,28 +330,26 @@ class LocationRepository @Inject constructor( userId = userIdProperty.value.toString() } if (userId != null) { - var user: User? = userHelper.read(userId) + var user: User? = userLocalDataSource.read(userId) // TODO : test the timer to make sure users are updated as needed! val sixHoursInMilliseconds = (6 * 60 * 60 * 1000).toLong() if (user == null || Date().after(Date(user.fetchedDate.time + sixHoursInMilliseconds))) { // get any users that were not recognized or expired Log.d(LOG_NAME, "User for location is null or stale, re-pulling") - userFetch.fetch(userId) - user = userHelper.read(userId) + userRepository.fetchUsers(listOf(userId)) + user = userLocalDataSource.read(userId) } location.user = user // if there is no existing location, create one - val l = locationHelper.read(location.remoteId) - if (l == null) { + val existingLocation = locationLocalDataSource.read(location.remoteId) + if (existingLocation == null) { // delete old location and create new one - if (user != null) { + if (user != null && user != currentUser) { // don't pull your own locations - if (user != currentUser) { - userId = user.id.toString() - val newLocation = locationHelper.create(location) - locationHelper.deleteUserLocations(userId, true, newLocation.event) - } + userId = user.id.toString() + val newLocation = locationLocalDataSource.create(location) + locationLocalDataSource.deleteUserLocations(userId, true, newLocation.event) } else { Log.w(LOG_NAME, "A location with no user was found and discarded. User id: $userId") } @@ -374,8 +357,6 @@ class LocationRepository @Inject constructor( } } } - - response } catch(e: Exception) { Log.e(LOG_NAME, "Failed to fetch user locations from server", e) } diff --git a/mage/src/main/java/mil/nga/giat/mage/data/observation/AttachmentRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/observation/AttachmentRepository.kt similarity index 80% rename from mage/src/main/java/mil/nga/giat/mage/data/observation/AttachmentRepository.kt rename to mage/src/main/java/mil/nga/giat/mage/data/repository/observation/AttachmentRepository.kt index 8cec1df32..9909e3061 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/observation/AttachmentRepository.kt +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/observation/AttachmentRepository.kt @@ -1,42 +1,42 @@ -package mil.nga.giat.mage.data.observation +package mil.nga.giat.mage.data.repository.observation -import android.content.Context +import android.app.Application import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.os.Environment import android.util.Log import androidx.exifinterface.media.ExifInterface import androidx.preference.PreferenceManager -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import mil.nga.giat.mage.R -import mil.nga.giat.mage.di.server5 -import mil.nga.giat.mage.network.api.AttachmentService -import mil.nga.giat.mage.network.api.AttachmentService_server5 +import mil.nga.giat.mage.di.Server5 +import mil.nga.giat.mage.network.attachment.AttachmentService +import mil.nga.giat.mage.network.attachment.AttachmentService_server5 import mil.nga.giat.mage.observation.sync.AttachmentSyncWorker import mil.nga.giat.mage.sdk.Compatibility -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.observation.AttachmentHelper +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource import mil.nga.giat.mage.sdk.utils.MediaUtility -import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody import java.io.File import java.io.InputStream import java.io.OutputStream +import java.util.UUID import javax.inject.Inject class AttachmentRepository @Inject constructor( - @ApplicationContext private val context: Context, + private val application: Application, private val attachmentService: AttachmentService, - @server5 private val attachmentService_server5: AttachmentService_server5 + private val attachmentLocalDataSource: AttachmentLocalDataSource, + @Server5 private val attachmentService_server5: AttachmentService_server5 ) { - private val attachmentHelper = AttachmentHelper.getInstance(context) - suspend fun syncAttachment(attachment: Attachment) = withContext(Dispatchers.IO) { Log.d(LOG_NAME, "Staging attachment with id: ${attachment.id}") stageForUpload(attachment) @@ -46,11 +46,11 @@ class AttachmentRepository @Inject constructor( val observationId = attachment.observation.remoteId val attachmentFile = File(attachment.localPath) val mediaTypeSpec = MediaUtility.getMimeType(attachment.localPath) ?: "application/octet-stream" - val mediaType = MediaType.parse(mediaTypeSpec) + val mediaType = mediaTypeSpec.toMediaTypeOrNull() val contentBody = RequestBody.create(mediaType, attachmentFile) val contentPart = MultipartBody.Part.createFormData("attachment", attachmentFile.name, contentBody) - val response = if (Compatibility.isServerVersion5(context)) { + val response = if (Compatibility.isServerVersion5(application)) { attachmentService_server5.createAttachment(eventId, observationId, contentPart) } else { attachmentService.createAttachment(eventId, observationId, attachment.remoteId, contentPart) @@ -69,9 +69,10 @@ class AttachmentRepository @Inject constructor( attachment.url = returnedAttachment?.url attachment.isDirty = false - AttachmentHelper.getInstance(context).update(attachment) + attachmentLocalDataSource.update(attachment) } else { - Log.e(LOG_NAME, "upload request failed for attachment ${attachment.remoteId} observation ${observationId} event ${eventId}\n" + + Log.e( + LOG_NAME, "upload request failed for attachment ${attachment.remoteId} observation ${observationId} event ${eventId}\n" + "-- ${response.code()}: ${response.errorBody()}") } @@ -80,8 +81,8 @@ class AttachmentRepository @Inject constructor( @Throws(Exception::class) fun stageForUpload(attachment: Attachment) { - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - val outImageSize = sharedPreferences.getInt(context.getString(R.string.imageUploadSizeKey), context.resources.getInteger(R.integer.imageUploadSizeDefaultValue)) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) + val outImageSize = sharedPreferences.getInt(application.getString(R.string.imageUploadSizeKey), application.resources.getInteger(R.integer.imageUploadSizeDefaultValue)) val file = File(attachment.localPath) val oldExif = ExifInterface(attachment.localPath) @@ -131,20 +132,10 @@ class AttachmentRepository @Inject constructor( } suspend fun download(attachment: Attachment, progressChannel: Channel) = withContext(Dispatchers.IO) { - val destination: File = when { - attachment.contentType.startsWith("image/") -> { - MediaUtility.createImageFile() - } - attachment.contentType.startsWith("video/") -> { - MediaUtility.createVideoFile() - } - attachment.contentType.startsWith("audio/") -> { - MediaUtility.createAudioFile() - } - else -> { - MediaUtility.createFile(MediaUtility.getFileExtension(attachment.name)) - } - } + val destination = File( + application.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + UUID.randomUUID().toString() + ) try { val observation = attachment.observation @@ -158,9 +149,9 @@ class AttachmentRepository @Inject constructor( attachment.localPath = destination.absolutePath streamFile(body.byteStream(), destination.outputStream(), contentLength, progressChannel) - val updateAttachment: Attachment = attachmentHelper.read(attachment.id) + val updateAttachment: Attachment = attachmentLocalDataSource.read(attachment.id) updateAttachment.localPath = attachment.localPath - attachmentHelper.update(updateAttachment) + attachmentLocalDataSource.update(updateAttachment) } destination @@ -197,7 +188,7 @@ class AttachmentRepository @Inject constructor( } suspend fun copyToCacheDir(file: File) = withContext(Dispatchers.IO) { - val directory = File(context.cacheDir, "attachments") + val directory = File(application.cacheDir, "attachments") if (!directory.exists()) { directory.mkdirs() diff --git a/mage/src/main/java/mil/nga/giat/mage/data/observation/ObservationRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/observation/ObservationRepository.kt similarity index 64% rename from mage/src/main/java/mil/nga/giat/mage/data/observation/ObservationRepository.kt rename to mage/src/main/java/mil/nga/giat/mage/data/repository/observation/ObservationRepository.kt index fa7dffd5b..400014dd3 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/observation/ObservationRepository.kt +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/observation/ObservationRepository.kt @@ -1,17 +1,19 @@ -package mil.nga.giat.mage.data.observation +package mil.nga.giat.mage.data.repository.observation import android.app.PendingIntent +import android.app.PendingIntent.FLAG_IMMUTABLE import android.content.Context import android.content.Intent import android.content.SharedPreferences -import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import com.google.gson.JsonObject import com.google.gson.JsonParser +import com.j256.ormlite.dao.Dao +import com.j256.ormlite.stmt.QueryBuilder +import com.j256.ormlite.stmt.Where import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.* import kotlinx.coroutines.channels.ProducerScope @@ -22,23 +24,28 @@ import kotlinx.coroutines.flow.flowOn import mil.nga.giat.mage.LandingActivity import mil.nga.giat.mage.MageApplication import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationError +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.State import mil.nga.giat.mage.filter.DateTimeFilter -import mil.nga.giat.mage.filter.FavoriteFilter import mil.nga.giat.mage.filter.Filter -import mil.nga.giat.mage.filter.ImportantFilter import mil.nga.giat.mage.form.FieldType import mil.nga.giat.mage.form.Form import mil.nga.giat.mage.form.field.Media -import mil.nga.giat.mage.network.api.ObservationService +import mil.nga.giat.mage.network.observation.ObservationService import mil.nga.giat.mage.sdk.Temporal -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.observation.* -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.database.model.observation.ObservationImportant import mil.nga.giat.mage.sdk.event.IObservationEventListener -import mil.nga.giat.mage.sdk.fetch.UserServerFetch import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import okhttp3.ResponseBody import retrofit2.Response +import java.io.IOException import java.net.HttpURLConnection import java.util.* import javax.inject.Inject @@ -46,17 +53,50 @@ import javax.inject.Inject class ObservationRepository @Inject constructor( @ApplicationContext private val context: Context, private val preferences: SharedPreferences, - private val observationService: ObservationService + private val observationService: ObservationService, + private val userRepository: UserRepository, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val observationFavoriteDao: Dao, + private val observationImportantDao: Dao, + private val observationLocalDataSource: ObservationLocalDataSource ) { - private val userHelper: UserHelper = UserHelper.getInstance(context) - private val eventHelper: EventHelper = EventHelper.getInstance(context) - private val observationHelper: ObservationHelper = ObservationHelper.getInstance(context) - private val observationDao = DaoStore.getInstance(context).observationDao - private var refreshTime: Long = 0 private var refreshJob: Job? = null private var oldestObservation: Observation? = null + private val favoriteFilter = object : Filter { + override fun query(): QueryBuilder? { + val user = userLocalDataSource.readCurrentUser() ?: return null + + val queryBuilder = observationFavoriteDao.queryBuilder() + queryBuilder.where() + .eq("user_id", user.remoteId) + .and() + .eq("is_favorite", true) + + return queryBuilder + } + + override fun passesFilter(observation: Observation): Boolean { + val user = userLocalDataSource.readCurrentUser() + return observation.favoritesMap[user?.remoteId]?.isFavorite == true + } + + override fun and(where: Where<*, Long>) {} + } + + private val importantFilter = object : Filter { + override fun query(): QueryBuilder { + val queryBuilder = observationImportantDao.queryBuilder() + queryBuilder.where().eq("is_important", true) + return queryBuilder + } + + override fun passesFilter(obj: Observation) = obj.important?.isImportant == true + override fun and(where: Where<*, Long>) {} + } + suspend fun create(observation: Observation) = withContext(Dispatchers.IO) { var response = observationService.createObservationId(observation.event.remoteId) @@ -69,14 +109,14 @@ class ObservationRepository @Inject constructor( response = update(observation) } else { observation.error = parseError(response) - ObservationHelper.getInstance(context).update(observation) + observationLocalDataSource.update(observation) } response } + @OptIn(ExperimentalCoroutinesApi::class) fun getObservations(): Flow> = callbackFlow { - val observationHelper = ObservationHelper.getInstance(context) val observationListener = object: IObservationEventListener { override fun onObservationCreated(observations: Collection, sendUserNotifcations: Boolean) { trySend(query(this@callbackFlow)) @@ -92,7 +132,7 @@ class ObservationRepository @Inject constructor( override fun onError(error: Throwable) {} } - observationHelper.addListener(observationListener) + observationLocalDataSource.addListener(observationListener) val observationFilterKey = context.resources.getString(R.string.activeTimeFilterKey) val preferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> @@ -105,41 +145,16 @@ class ObservationRepository @Inject constructor( send(query(this)) awaitClose { - observationHelper.removeListener(observationListener) + observationLocalDataSource.removeListener(observationListener) preferences.unregisterOnSharedPreferenceChangeListener(preferencesListener) } }.flowOn(Dispatchers.IO) + @OptIn(ExperimentalCoroutinesApi::class) private fun query(scope: ProducerScope>): List { - val event = eventHelper.currentEvent - val dao = DaoStore.getInstance(context).observationDao - val query = dao.queryBuilder() - val where = query - .orderBy("timestamp", false) - .where() - .eq("event_id", event.id) - - getTemporalFilter()?.let { filter -> - filter.query()?.let { query.join(it) } - filter.and(where) - } - - getImportantFilter()?.let { filter -> - filter.query()?.let { query.join(it) } - filter.and(where) - } - - getFavoriteFilter()?.let { filter -> - filter.query()?.let { query.join(it) } - filter.and(where) - } - - val iterator = observationDao.iterator(query.prepare()) - - val observations = mutableListOf() - while(iterator.hasNext()) { - observations.add(iterator.current()) - } + val event = eventLocalDataSource.currentEvent ?: return emptyList() + val filters = listOfNotNull(getTemporalFilter(), getImportantFilter(), getFavoriteFilter()) + val observations = observationLocalDataSource.getEventObservations(event, filters) observations.lastOrNull()?.let { observation -> if (oldestObservation == null || oldestObservation?.timestamp?.after(observation.lastModified) == true) { @@ -208,14 +223,15 @@ class ObservationRepository @Inject constructor( } private fun getImportantFilter(): Filter? { - return if (preferences.getBoolean(context.resources.getString(R.string.activeImportantFilterKey), false)) { - ImportantFilter(context) + val filter = preferences.getBoolean(context.resources.getString(R.string.activeImportantFilterKey), false) + return if (filter) { + importantFilter } else null } private fun getFavoriteFilter(): Filter? { return if (preferences.getBoolean(context.resources.getString(R.string.activeFavoritesFilterKey), false)) { - FavoriteFilter(context) + favoriteFilter } else null } @@ -242,7 +258,7 @@ class ObservationRepository @Inject constructor( // Mark new attachments as dirty and set local path for upload for (observationForm in observation.forms) { - eventHelper.getForm(observationForm.formId)?.json?.let { formJson -> + eventLocalDataSource.getForm(observationForm.formId)?.json?.let { formJson -> val formDefinition = Form.fromJson(formJson) for (observationProperty in observationForm.properties) { val fieldDefinition = formDefinition?.fields?.find { it.name == observationProperty.key } @@ -267,10 +283,10 @@ class ObservationRepository @Inject constructor( } } - ObservationHelper.getInstance(context).update(returnedObservation) + returnedObservation?.let { observationLocalDataSource.update(it) } } else { observation.error = parseError(response) - ObservationHelper.getInstance(context).update(observation) + observationLocalDataSource.update(observation) } response @@ -283,11 +299,11 @@ class ObservationRepository @Inject constructor( val response = observationService.archiveObservation(observation.event.remoteId, observation.remoteId, state) when { response.isSuccessful || response.code() == HttpURLConnection.HTTP_NOT_FOUND -> { - observationHelper.delete(observation) + observationLocalDataSource.delete(observation) } response.code() != HttpURLConnection.HTTP_UNAUTHORIZED -> { observation.error = parseError(response) - ObservationHelper.getInstance(context).update(observation) + observationLocalDataSource.update(observation) } } @@ -306,7 +322,7 @@ class ObservationRepository @Inject constructor( if (response.isSuccessful) { val returnedObservation = response.body() observation.lastModified = returnedObservation?.lastModified - observationHelper.updateImportant(observation) + observationLocalDataSource.updateImportant(observation) } response @@ -324,7 +340,7 @@ class ObservationRepository @Inject constructor( if (response.isSuccessful) { val updatedObservation = response.body() observation.lastModified = updatedObservation?.lastModified - observationHelper.updateFavorite(favorite) + observationLocalDataSource.updateFavorite(favorite) } response @@ -333,18 +349,19 @@ class ObservationRepository @Inject constructor( suspend fun fetch(notify: Boolean) = withContext(Dispatchers.IO) { val fetched = mutableListOf() - val event = EventHelper.getInstance(context).currentEvent - Log.d(LOG_NAME, "Fetch observations for event " + event.name) + val currentUser = userLocalDataSource.readCurrentUser() ?: return@withContext + val currentEvent = eventLocalDataSource.currentEvent ?: return@withContext + Log.d(LOG_NAME, "Fetch observations for event " + currentEvent.name) try { - val lastModifiedDate = observationHelper.getLatestCleanLastModified(context, event) + val lastModifiedDate = observationLocalDataSource.getLatestCleanLastModified(currentUser, currentEvent) val iso8601Format = ISO8601DateFormatFactory.ISO8601() - val response = observationService.getObservations(event.remoteId, iso8601Format.format(lastModifiedDate)) + val response = observationService.getObservations(currentEvent.remoteId, iso8601Format.format(lastModifiedDate)) if (response.isSuccessful) { - val observations = response.body()!!.map { - it.event = event + val observations = response.body()?.map { + it.event = currentEvent it - }.toMutableList() + }?.toMutableList() ?: mutableListOf() Log.d(LOG_NAME, "Fetched " + observations.size + " new observations") @@ -352,29 +369,29 @@ class ObservationRepository @Inject constructor( while(iterator.hasNext()) { val observation = iterator.next() - val userId = observation.userId - if (userId != null) { - val user = userHelper.read(userId) + observation.userId?.let { userId -> + val user = userLocalDataSource.read(userId) // TODO : test the timer to make sure users are updated as needed! val sixHoursInMilliseconds = (6 * 60 * 60 * 1000).toLong() if (user == null || Date().after(Date(user.fetchedDate.time + sixHoursInMilliseconds))) { // get any users that were not recognized or expired Log.d(LOG_NAME, "User for observation is null or stale, re-pulling") - UserServerFetch(context).fetch(userId) + userRepository.fetchUsers(listOf(userId)) } } - val oldObservation = observationHelper.read(observation.remoteId) + val oldObservation = observationLocalDataSource.read(observation.remoteId) if (observation.state == State.ARCHIVE && oldObservation != null) { - observationHelper.delete(oldObservation) + observationLocalDataSource.delete(oldObservation) Log.d(LOG_NAME, "Deleted observation with remote_id " + observation.remoteId) } else if (observation.state != State.ARCHIVE && oldObservation == null) { - val newObservation = observationHelper.create(observation, false) - fetched.add(newObservation) - Log.d(LOG_NAME, "Created observation with remote_id " + newObservation.remoteId) + observationLocalDataSource.create(observation, false)?.let { + fetched.add(it) + Log.d(LOG_NAME, "Created observation with remote_id " + it.remoteId) + } } else if (observation.state != State.ARCHIVE && oldObservation != null && !oldObservation.isDirty) { // TODO : conflict resolution observation.id = oldObservation.id - observationHelper.update(observation) + observationLocalDataSource.update(observation) Log.d(LOG_NAME, "Updated observation with remote_id " + observation.remoteId) } @@ -398,75 +415,72 @@ class ObservationRepository @Inject constructor( } val notificationManager = NotificationManagerCompat.from(context) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { - - val viewIntent = Intent(context, LandingActivity::class.java) - val viewPendingIntent = PendingIntent.getActivity(context, 0, viewIntent, 0) - - val content = if (observations.size == 1) "New observation was created in ${observations.first().event.name}" else "${observations.size} new observations were created in ${observations.first().event.name}" - - val notificationBuilder = NotificationCompat.Builder(context, MageApplication.MAGE_NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_new_obs) - .setContentTitle("New MAGE Observation(s)") - .setContentText(content) - .setVibrate(longArrayOf(0, 400, 75, 250, 75, 250)) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setAutoCancel(true) - .setContentIntent(viewPendingIntent) - - notificationManager.notify(MageApplication.MAGE_OBSERVATION_NOTIFICATION_PREFIX, notificationBuilder.build()) - } else { - val groupNotification = NotificationCompat.Builder(context, MageApplication.MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID) - .setGroupSummary(true) - .setContentTitle("New MAGE Observations") - .setContentText("Some other text") - .setSmallIcon(R.drawable.ic_place_black_24dp) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setGroup(MageApplication.MAGE_OBSERVATION_NOTIFICATION_GROUP) - - notificationManager.notify(MageApplication.MAGE_OBSERVATION_NOTIFICATION_PREFIX, groupNotification.build()) - - observations.forEach { observation -> - val intent = Intent(context, LandingActivity::class.java) - val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) - - val information = mutableListOf() - observation.forms.firstOrNull()?.let { observationForm -> - eventHelper.getForm(observationForm.formId)?.let { form -> - if (form.primaryFeedField != null) { - val property = observationForm.properties.find { it.key == form.primaryFeedField } - property?.value?.toString()?.let { - information.add(it) - } + val groupNotification = NotificationCompat.Builder(context, MageApplication.MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID) + .setGroupSummary(true) + .setContentTitle("New MAGE Observations") + .setContentText("Some other text") + .setSmallIcon(R.drawable.ic_place_black_24dp) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setGroup(MageApplication.MAGE_OBSERVATION_NOTIFICATION_GROUP) + + notificationManager.notify(MageApplication.MAGE_OBSERVATION_NOTIFICATION_PREFIX, groupNotification.build()) + + observations.forEach { observation -> + val intent = Intent(context, LandingActivity::class.java) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAG_IMMUTABLE) + + val information = mutableListOf() + observation.forms.firstOrNull()?.let { observationForm -> + eventLocalDataSource.getForm(observationForm.formId)?.let { form -> + if (form.primaryFeedField != null) { + val property = observationForm.properties.find { it.key == form.primaryFeedField } + property?.value?.toString()?.let { + information.add(it) } + } - if (form.secondaryFeedField != null) { - val property = observationForm.properties.find { it.key == form.secondaryFeedField } - property?.value?.toString()?.let { - information.add(it) - } + if (form.secondaryFeedField != null) { + val property = observationForm.properties.find { it.key == form.secondaryFeedField } + property?.value?.toString()?.let { + information.add(it) } } } + } + + val content = if (information.isNotEmpty()) "${information.joinToString(", ")} was created in ${observation.event.name}" else "Observation was created in ${observation.event.name}" - val content = if (information.isNotEmpty()) "${information.joinToString(", ")} was created in ${observation.event.name}" else "Observation was created in ${observation.event.name}" + val notificationBuilder = NotificationCompat.Builder(context, MageApplication.MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_place_black_24dp) + .setContentTitle("New Observation") + .setContentText(content) + .setAutoCancel(true) + .setGroup(MageApplication.MAGE_OBSERVATION_NOTIFICATION_GROUP) + .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) + .setContentIntent(pendingIntent) - val notificationBuilder = NotificationCompat.Builder(context, MageApplication.MAGE_OBSERVATION_NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_place_black_24dp) - .setContentTitle("New Observation") - .setContentText(content) - .setAutoCancel(true) - .setGroup(MageApplication.MAGE_OBSERVATION_NOTIFICATION_GROUP) - .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) - .setContentIntent(pendingIntent) + notificationManager.notify(MageApplication.MAGE_OBSERVATION_NOTIFICATION_PREFIX + observation.id.toInt(), notificationBuilder.build()) + } + } - notificationManager.notify(MageApplication.MAGE_OBSERVATION_NOTIFICATION_PREFIX + observation.id.toInt(), notificationBuilder.build()) - } + @Throws(IOException::class) + suspend fun getAttachment(attachment: Attachment): ResponseBody? { + val eventId = attachment.observation.event.remoteId + val observationId = attachment.observation.remoteId + val attachmentId = attachment.remoteId + val response = observationService.getAttachment(eventId, observationId, attachmentId) + if (response.isSuccessful) { + return response.body() + } else { + Log.e(LOG_NAME, "Error fetching attachment $attachment") + response.errorBody()?.let { Log.e(LOG_NAME, it.string()) } } + return null } private fun parseError(response: Response): ObservationError { - val observationError = ObservationError() + val observationError = + ObservationError() observationError.statusCode = response.code() observationError.description = response.message() observationError.message = response.errorBody()?.string() diff --git a/mage/src/main/java/mil/nga/giat/mage/data/repository/user/UserRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/repository/user/UserRepository.kt new file mode 100644 index 000000000..3c87dd4a9 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/data/repository/user/UserRepository.kt @@ -0,0 +1,346 @@ +package mil.nga.giat.mage.data.repository.user + +import android.app.Application +import android.content.SharedPreferences +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import androidx.lifecycle.viewModelScope +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.stream.JsonReader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.datasource.permission.RoleLocalDataSource +import mil.nga.giat.mage.di.TokenProvider +import mil.nga.giat.mage.login.AuthenticationStatus +import mil.nga.giat.mage.login.AuthorizationStatus +import mil.nga.giat.mage.network.device.DeviceService +import mil.nga.giat.mage.network.user.UserService +import mil.nga.giat.mage.sdk.Compatibility.Companion.isCompatibleWith +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.network.user.UserWithRoleTypeAdapter +import mil.nga.giat.mage.sdk.utils.DeviceUuidFactory +import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import mil.nga.giat.mage.sdk.utils.MediaUtility +import mil.nga.giat.mage.sdk.utils.PasswordUtility +import okhttp3.MediaType +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Response +import java.io.File +import java.io.StringReader +import java.util.Base64 +import java.util.Date +import javax.inject.Inject + +class UserRepository @Inject constructor( + private val application: Application, + private val preferences: SharedPreferences, + private val userService: UserService, + private val deviceService: DeviceService, + private val tokenProvider: TokenProvider, + private val roleLocalDataSource: RoleLocalDataSource, + private val userLocalDataSource: UserLocalDataSource +) { + + suspend fun authenticateLocal(strategy: String, username: String, password: String): AuthenticationStatus { + val parameters = JsonObject() + parameters.addProperty("username", username) + parameters.addProperty("password", password) + + try { + val packageInfo = application.packageManager.getPackageInfo(application.packageName, 0) + parameters.addProperty( + "appVersion", + String.format("%s-%s", packageInfo.versionName, packageInfo.versionCode) + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(LOG_NAME, "Problem sending package info on local signin", e) + } + + val status = try { + val response = userService.signin(strategy, parameters) + if (response.isSuccessful) { + val json = response.body()!! + AuthenticationStatus.Success( + username = username, + token = json["token"].asString + ) + } else if (response.code() == 403) { + val message = response.errorBody()?.string() ?: "User account is not approved, please contact your MAGE administrator to approve your account." + AuthenticationStatus.AccountCreated(message = message) + } else { + val message = response.errorBody()?.string() ?: "Please check your username and password and try again." + AuthenticationStatus.Failure( + code = response.code(), + message = message + ) + } + } catch (e: Exception) { + val message = "Error connecting to server, please contact your MAGE administrator" + val offlineUsername = preferences.getString(application.getString(R.string.usernameKey), null) + val offlinePassword = preferences.getString(application.getString(R.string.passwordHashKey), null) + if (offlineUsername == username && PasswordUtility.equal(password, offlinePassword)) { + AuthenticationStatus.Offline(message) + } else { + AuthenticationStatus.Failure( + code = 500, + message = message + ) + } + } + + return status + } + + fun authenticateOffline() { + val tokenExpirationLength = preferences.getLong(application.getString(R.string.tokenExpirationLengthKey), 0).coerceAtLeast(0) + val tokenExpiration = Date(System.currentTimeMillis() + tokenExpirationLength) + preferences.edit().putString( + application.getString(R.string.tokenExpirationDateKey), + ISO8601DateFormatFactory.ISO8601().format(tokenExpiration) + ).apply() + } + + suspend fun authorize(jwt: String): AuthorizationStatus { + val parameters = JsonObject() + parameters.addProperty("uid", DeviceUuidFactory(application).deviceUuid.toString()) + + val (_, payload) = jwt.split(".") + val json = String(Base64.getDecoder().decode(payload)) + val authorizedUser = Gson().fromJson(json, JsonObject::class.java)["sub"].asString?.let { subject -> + userLocalDataSource.read(subject) + } + + return try { + val authorizeResponse = deviceService.authorize( + String.format("Bearer %s", jwt), + System.getProperty("http.agent"), + parameters + ) + + if (authorizeResponse.isSuccessful) { + val authorization = authorizeResponse.body() + + // Check server api version to ensure compatibility before continuing + val serverVersion = authorization!!["api"].asJsonObject["version"].asJsonObject + if (!isCompatibleWith(serverVersion["major"].asInt, serverVersion["minor"].asInt)) { + Log.e(LOG_NAME, "Server version not compatible") + AuthorizationStatus.FailInvalidServer + } + + // Successful login, put the token information in the shared preferences + val token = authorization["token"].asString + val tokenExpiration = authorization["expirationDate"].asString.trim() + val userJson = authorization.getAsJsonObject("user") + val reader = JsonReader(StringReader(userJson.toString())) + val userWithRole = UserWithRoleTypeAdapter().read(reader) + roleLocalDataSource.read(userWithRole.role.remoteId)?.let { userWithRole.role.id = it.id } + + val role = roleLocalDataSource.createOrUpdate(userWithRole.role) + userWithRole.user.role = role + userWithRole.user.fetchedDate = Date() + val user = userLocalDataSource.createOrUpdate(userWithRole.user) + userLocalDataSource.setCurrentUser(user) + + AuthorizationStatus.Success( + user = user, + token = token, + tokenExpiration = tokenExpiration + ) + } else { + val code = authorizeResponse.code() + if (code == 403) { + AuthorizationStatus.FailAuthorization(user = authorizedUser) + } else { + AuthorizationStatus.FailAuthentication( + user = authorizedUser, + message = "Authorization error, please contact your MAGE administrator for assistance" + ) + } + } + } catch (e: Exception) { + Log.e(LOG_NAME, "Error authorizing device", e) + AuthorizationStatus.FailAuthorization() + } + } + + fun signout() { + try { + userService.signout() + tokenProvider.signout() + } catch (e: Exception) { + Log.e(LOG_NAME, "Error signing out", e) + } + } + + suspend fun getCaptcha(username: String, background: String): Response { + val json = JsonObject() + json.addProperty("username", username) + json.addProperty("background", background) + return userService.signup(json) + } + + suspend fun verifyUser( + displayName: String?, + email: String?, + phone: String?, + password: String?, + captchaText: String?, + token: String? + ): Response { + val json = JsonObject() + json.addProperty("displayName", displayName) + json.addProperty("email", email) + json.addProperty("phone", phone) + json.addProperty("password", password) + json.addProperty("passwordconfirm", password) + json.addProperty("captchaText", captchaText) + return userService.signupVerify(String.format("Bearer %s", token), json) + } + + suspend fun fetchUsers(ids: List) { + ids.filter { it != "-1" }.forEach { id -> + val response = userService.getUser(id) + if (response.isSuccessful) { + response.body()?.let { (user, role) -> + roleLocalDataSource.read(role.remoteId).let { user.role = it } + user.fetchedDate = Date() + roleLocalDataSource.createOrUpdate(role) + userLocalDataSource.createOrUpdate(user) + } + } + } + } + + suspend fun changePassword( + username: String?, + password: String?, + newPassword: String?, + newPasswordConfirm: String? + ): Response { + val json = JsonObject() + json.addProperty("username", username) + json.addProperty("password", password) + json.addProperty("newPassword", newPassword) + json.addProperty("newPasswordConfirm", newPasswordConfirm) + return userService.changePassword(json) + } + + suspend fun syncAvatar(path: String) = withContext(Dispatchers.IO) { + Log.i(LOG_NAME, "Pushing user avatar $path") + + try { + val parts: MutableMap = HashMap() + val avatar = File(path) + val mimeType = MediaUtility.getMimeType(path) + val fileBody = RequestBody.create(mimeType.toMediaTypeOrNull(), avatar) + parts["avatar\"; filename=\"" + avatar.name + "\""] = fileBody + val response = userService.createAvatar(parts) + if (response.isSuccessful) { + response.body()?.let { (user, role) -> + roleLocalDataSource.createOrUpdate(role) + userLocalDataSource.readCurrentUser()?.let { currentUser -> + currentUser.avatarUrl = user.avatarUrl + currentUser.lastModified = Date(currentUser.lastModified.time + 1) + userLocalDataSource.update(currentUser) + userLocalDataSource.setAvatarPath(currentUser, null) + Log.d(LOG_NAME, "Updated user with remote_id " + user.remoteId) + } + } + } else { + Log.e(LOG_NAME, "Bad request.") + response.errorBody()?.let { error -> + Log.e(LOG_NAME, error.string()) + } + } + } catch (e: java.lang.Exception) { + Log.e(LOG_NAME, "Failure saving observation.", e) + } + } + + suspend fun syncIcons(event: Event) = withContext(Dispatchers.IO) { + val users = userLocalDataSource.getUsersInEvent(event) + for (user in users) { + try { + syncIcon(user) + } catch (e: Exception) { + Log.e(LOG_NAME, "Error syncing user icon", e) + } + } + } + + private suspend fun syncIcon(user: User) { + val path = "${MediaUtility.getUserIconDirectory(application)}/${user.id}.png" + val file = File(path) + if (file.exists()) { + file.delete() + } + + val response = userService.getIcon(user.remoteId) + val body = response.body() + if (response.isSuccessful && body != null) { + saveIcon(body, file) + compressIcon(file) + userLocalDataSource.setIconPath(user, path) + } + } + + private fun saveIcon(body: ResponseBody, file: File) { + body.let { + it.byteStream().use { input -> + file.outputStream().use { output -> + input.copyTo(output) + } + } + } + } + + private fun compressIcon(file: File) { + val sampleSize = getSampleSize(file) + + file.inputStream().use { inputStream -> + val options = BitmapFactory.Options() + options.inSampleSize = sampleSize + val bitmap = BitmapFactory.decodeStream(inputStream, null, options) + bitmap?.let { + file.outputStream().use { outputStream -> + it.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } + } + } + } + + private fun getSampleSize(file: File): Int { + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + return file.inputStream().use { inputStream -> + BitmapFactory.decodeStream(inputStream, null, options) + + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + if (height > MAX_DIMENSION || width > MAX_DIMENSION) { + // Calculate the largest inSampleSize value that is a power of 2 and will ensure + // height and width is smaller than the max image we can process + while (height / inSampleSize >= MAX_DIMENSION && height / inSampleSize >= MAX_DIMENSION) { + inSampleSize *= 2 + } + } + + inSampleSize + } + } + + companion object { + private val LOG_NAME = UserRepository::class.java.name + private const val MAX_DIMENSION = 200 + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/user/UserRepository.kt b/mage/src/main/java/mil/nga/giat/mage/data/user/UserRepository.kt deleted file mode 100644 index 432ecd75b..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/data/user/UserRepository.kt +++ /dev/null @@ -1,102 +0,0 @@ -package mil.nga.giat.mage.data.user - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.util.Log -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import mil.nga.giat.mage.network.api.UserService -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper -import mil.nga.giat.mage.sdk.utils.MediaUtility -import okhttp3.ResponseBody -import java.io.File -import javax.inject.Inject - -class UserRepository @Inject constructor( - @ApplicationContext private val context: Context, - private val userService: UserService -) { - private val userHelper = UserHelper.getInstance(context) - - suspend fun syncIcons(event: Event) = withContext(Dispatchers.IO) { - val users = userHelper.getUsersInEvent(event) - for (user in users) { - try { - syncIcon(user) - } catch (e: Exception) { - Log.e(LOG_NAME, "Error syncing user icon", e) - } - } - } - - private suspend fun syncIcon(user: User) { - val path = "${MediaUtility.getUserIconDirectory(context)}/${user.id}.png" - val file = File(path) - if (file.exists()) { - file.delete() - } - - val response = userService.getIcon(user.remoteId) - val body = response.body() - if (response.isSuccessful && body != null) { - saveIcon(body, file) - compressIcon(file) - userHelper.setIconPath(user, path) - } - } - - private fun saveIcon(body: ResponseBody, file: File) { - body.let { - it.byteStream().use { input -> - file.outputStream().use { output -> - input.copyTo(output) - } - } - } - } - - private fun compressIcon(file: File) { - val sampleSize = getSampleSize(file) - - file.inputStream().use { inputStream -> - val options = BitmapFactory.Options() - options.inSampleSize = sampleSize - val bitmap = BitmapFactory.decodeStream(inputStream, null, options) - bitmap?.let { - file.outputStream().use { outputStream -> - it.compress(Bitmap.CompressFormat.PNG, 100, outputStream) - } - } - } - } - - private fun getSampleSize(file: File): Int { - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - return file.inputStream().use { inputStream -> - BitmapFactory.decodeStream(inputStream, null, options) - - val height = options.outHeight - val width = options.outWidth - var inSampleSize = 1 - if (height > MAX_DIMENSION || width > MAX_DIMENSION) { - // Calculate the largest inSampleSize value that is a power of 2 and will ensure - // height and width is smaller than the max image we can process - while (height / inSampleSize >= MAX_DIMENSION && height / inSampleSize >= MAX_DIMENSION) { - inSampleSize *= 2 - } - } - - inSampleSize - } - } - - companion object { - private val LOG_NAME = UserRepository::class.java.name - private const val MAX_DIMENSION = 200 - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/converters/DateTypeConverter.kt b/mage/src/main/java/mil/nga/giat/mage/database/DateTypeConverter.kt similarity index 87% rename from mage/src/main/java/mil/nga/giat/mage/data/converters/DateTypeConverter.kt rename to mage/src/main/java/mil/nga/giat/mage/database/DateTypeConverter.kt index 878e2c2be..c83183396 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/converters/DateTypeConverter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/DateTypeConverter.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.converters +package mil.nga.giat.mage.database import androidx.room.TypeConverter import java.util.* diff --git a/mage/src/main/java/mil/nga/giat/mage/database/GeometryTypeConverter.kt b/mage/src/main/java/mil/nga/giat/mage/database/GeometryTypeConverter.kt new file mode 100644 index 000000000..4e7c73e9c --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/GeometryTypeConverter.kt @@ -0,0 +1,20 @@ +package mil.nga.giat.mage.database + +import androidx.room.TypeConverter +import mil.nga.giat.mage.sdk.utils.toBytes +import mil.nga.giat.mage.sdk.utils.toGeometry +import mil.nga.sf.Geometry + +class GeometryTypeConverter { + + @TypeConverter + fun fromByteArray(value: ByteArray?): Geometry? { + return value?.toGeometry() + } + + @TypeConverter + fun fromGeometry(geometry: Geometry?): ByteArray? { + return geometry?.toBytes() + } + +} diff --git a/mage/src/main/java/mil/nga/giat/mage/data/converters/JsonTypeConverter.kt b/mage/src/main/java/mil/nga/giat/mage/database/JsonTypeConverter.kt similarity index 86% rename from mage/src/main/java/mil/nga/giat/mage/data/converters/JsonTypeConverter.kt rename to mage/src/main/java/mil/nga/giat/mage/database/JsonTypeConverter.kt index a6ca7f36d..073e31bed 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/converters/JsonTypeConverter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/JsonTypeConverter.kt @@ -1,9 +1,8 @@ -package mil.nga.giat.mage.data.converters +package mil.nga.giat.mage.database import androidx.room.TypeConverter import com.google.gson.Gson import com.google.gson.JsonElement -import com.google.gson.JsonObject class JsonTypeConverter { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/MageDatabase.kt b/mage/src/main/java/mil/nga/giat/mage/database/MageDatabase.kt similarity index 61% rename from mage/src/main/java/mil/nga/giat/mage/data/MageDatabase.kt rename to mage/src/main/java/mil/nga/giat/mage/database/MageDatabase.kt index aa6640614..174a3e003 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/MageDatabase.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/MageDatabase.kt @@ -1,14 +1,15 @@ -package mil.nga.giat.mage.data +package mil.nga.giat.mage.database -import android.content.Context import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters -import mil.nga.giat.mage.data.converters.DateTypeConverter -import mil.nga.giat.mage.data.converters.GeometryTypeConverter -import mil.nga.giat.mage.data.converters.JsonTypeConverter -import mil.nga.giat.mage.data.feed.* -import mil.nga.giat.mage.sdk.datastore.DaoStore +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.dao.feed.FeedItemDao +import mil.nga.giat.mage.database.dao.feed.FeedLocalDao +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedItem +import mil.nga.giat.mage.database.model.feed.FeedLocal +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper @Database( version = MageDatabase.VERSION, @@ -29,9 +30,7 @@ abstract class MageDatabase : RoomDatabase() { abstract fun feedLocalDao(): FeedLocalDao abstract fun feedItemDao(): FeedItemDao - fun destroy(context: Context) { - DaoStore.getInstance(context).resetDatabase() - + fun destroy() { feedDao().destroy() feedLocalDao().destroy() feedItemDao().destroy() diff --git a/mage/src/main/java/mil/nga/giat/mage/database/dao/MageSqliteOpenHelper.kt b/mage/src/main/java/mil/nga/giat/mage/database/dao/MageSqliteOpenHelper.kt new file mode 100644 index 000000000..98eb28c9a --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/dao/MageSqliteOpenHelper.kt @@ -0,0 +1,122 @@ +package mil.nga.giat.mage.database.dao + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.util.Log +import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper +import com.j256.ormlite.field.DataPersisterManager +import com.j256.ormlite.support.ConnectionSource +import com.j256.ormlite.table.TableUtils +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.feature.StaticFeatureProperty +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.location.LocationProperty +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationErrorPersister +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.permission.Role +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.database.model.user.UserLocal +import mil.nga.giat.mage.database.model.user.UserTeam +import java.sql.SQLException +import kotlin.Int +import kotlin.Throws + +class MageSqliteOpenHelper( + context: Context +) : OrmLiteSqliteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + @Throws(SQLException::class) + private fun createTables() { + TableUtils.createTable(connectionSource, Observation::class.java) + TableUtils.createTable(connectionSource, ObservationForm::class.java) + TableUtils.createTable(connectionSource, ObservationProperty::class.java) + TableUtils.createTable(connectionSource, ObservationImportant::class.java) + TableUtils.createTable(connectionSource, ObservationFavorite::class.java) + TableUtils.createTable(connectionSource, Attachment::class.java) + TableUtils.createTable(connectionSource, User::class.java) + TableUtils.createTable(connectionSource, UserLocal::class.java) + TableUtils.createTable(connectionSource, Role::class.java) + TableUtils.createTable(connectionSource, Event::class.java) + TableUtils.createTable(connectionSource, Form::class.java) + TableUtils.createTable(connectionSource, Team::class.java) + TableUtils.createTable(connectionSource, UserTeam::class.java) + TableUtils.createTable(connectionSource, TeamEvent::class.java) + TableUtils.createTable(connectionSource, Location::class.java) + TableUtils.createTable(connectionSource, LocationProperty::class.java) + TableUtils.createTable(connectionSource, Layer::class.java) + TableUtils.createTable(connectionSource, StaticFeature::class.java) + TableUtils.createTable(connectionSource, StaticFeatureProperty::class.java) + DataPersisterManager.registerDataPersisters(ObservationErrorPersister.singleton) + } + + override fun onCreate(sqliteDatabase: SQLiteDatabase, connectionSource: ConnectionSource) { + try { + createTables() + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not create tables.", e) + } + } + + @Throws(SQLException::class) + private fun dropTables() { + TableUtils.dropTable(connectionSource, Observation::class.java, true) + TableUtils.dropTable(connectionSource, ObservationForm::class.java, true) + TableUtils.dropTable(connectionSource, ObservationProperty::class.java, true) + TableUtils.dropTable(connectionSource, ObservationImportant::class.java, true) + TableUtils.dropTable(connectionSource, ObservationFavorite::class.java, true) + TableUtils.dropTable(connectionSource, Attachment::class.java, true) + TableUtils.dropTable(connectionSource, User::class.java, true) + TableUtils.dropTable(connectionSource, UserLocal::class.java, true) + TableUtils.dropTable(connectionSource, Role::class.java, true) + TableUtils.dropTable(connectionSource, Event::class.java, true) + TableUtils.dropTable(connectionSource, Form::class.java, true) + TableUtils.dropTable(connectionSource, Team::class.java, true) + TableUtils.dropTable(connectionSource, UserTeam::class.java, true) + TableUtils.dropTable(connectionSource, TeamEvent::class.java, true) + TableUtils.dropTable(connectionSource, Location::class.java, true) + TableUtils.dropTable(connectionSource, LocationProperty::class.java, true) + TableUtils.dropTable(connectionSource, Layer::class.java, true) + TableUtils.dropTable(connectionSource, StaticFeature::class.java, true) + TableUtils.dropTable(connectionSource, StaticFeatureProperty::class.java, true) + } + + override fun onUpgrade( + database: SQLiteDatabase, + connectionSource: ConnectionSource, + oldVersion: Int, + newVersion: Int + ) { + resetDatabase() + } + + /** + * Drop and create all tables. + */ + fun resetDatabase() { + try { + Log.d(LOG_NAME, "Reseting Database.") + dropTables() + createTables() + Log.d(LOG_NAME, "Reset Database.") + } catch (e: SQLException) { + Log.e(LOG_NAME, "Could not reset Database.", e) + } + } + + companion object { + private const val DATABASE_NAME = "mage.db" + private val LOG_NAME = MageSqliteOpenHelper::class.java.name + + const val DATABASE_VERSION = 22 + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedDao.kt b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedDao.kt similarity index 90% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedDao.kt rename to mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedDao.kt index c7ccf48b1..b8289f607 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedDao.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedDao.kt @@ -1,7 +1,9 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.dao.feed import androidx.lifecycle.LiveData import androidx.room.* +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedWithItems @Dao interface FeedDao { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemDao.kt b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedItemDao.kt similarity index 88% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemDao.kt rename to mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedItemDao.kt index 6ec7db836..8163f6ba7 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemDao.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedItemDao.kt @@ -1,9 +1,12 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.dao.feed import androidx.lifecycle.LiveData import androidx.paging.PagingSource import androidx.room.* import kotlinx.coroutines.flow.Flow +import mil.nga.giat.mage.database.model.feed.FeedItem +import mil.nga.giat.mage.database.model.feed.FeedWithItems +import mil.nga.giat.mage.database.model.feed.ItemWithFeed @Dao interface FeedItemDao { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocalDao.kt b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedLocalDao.kt similarity index 78% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocalDao.kt rename to mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedLocalDao.kt index 1b1fccc59..2d99d64d4 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocalDao.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/dao/feed/FeedLocalDao.kt @@ -1,7 +1,8 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.dao.feed -import androidx.lifecycle.LiveData import androidx.room.* +import mil.nga.giat.mage.database.model.feed.FeedAndLocal +import mil.nga.giat.mage.database.model.feed.FeedLocal @Dao interface FeedLocalDao { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/Property.java b/mage/src/main/java/mil/nga/giat/mage/database/model/Property.java similarity index 97% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/Property.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/Property.java index 016ce249e..5dfc8ebc0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/Property.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/Property.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore; +package mil.nga.giat.mage.database.model; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Event.java b/mage/src/main/java/mil/nga/giat/mage/database/model/event/Event.java similarity index 98% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Event.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/event/Event.java index 0100f4514..cae8c5a15 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Event.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/event/Event.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.event; import com.google.gson.JsonObject; import com.google.gson.JsonParser; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Form.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/event/Form.kt similarity index 95% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Form.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/event/Form.kt index eb277039c..d1a3f8ebb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Form.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/event/Form.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user +package mil.nga.giat.mage.database.model.event import com.j256.ormlite.field.DatabaseField import com.j256.ormlite.table.DatabaseTable diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeature.java b/mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeature.java similarity index 91% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeature.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeature.java index 56476ded4..1ea800a61 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeature.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeature.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.staticfeature; +package mil.nga.giat.mage.database.model.feature; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; @@ -14,8 +14,8 @@ import java.util.HashMap; import java.util.Map; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.utils.GeometryUtility; +import mil.nga.giat.mage.database.model.layer.Layer; +import mil.nga.giat.mage.sdk.utils.GeometryUtilityKt; import mil.nga.sf.Geometry; @DatabaseTable(tableName = "staticfeatures") @@ -54,7 +54,7 @@ public StaticFeature(Geometry geometry, Layer layer) { public StaticFeature(String remoteId, Geometry geometry, Layer layer) { super(); this.remoteId = remoteId; - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); this.layer = layer; } @@ -87,11 +87,11 @@ public void setGeometryBytes(byte[] geometryBytes) { } public Geometry getGeometry() { - return GeometryUtility.toGeometry(getGeometryBytes()); + return GeometryUtilityKt.toGeometry(getGeometryBytes()); } public void setGeometry(Geometry geometry) { - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); } public Collection getProperties() { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureProperty.java b/mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeatureProperty.java similarity index 89% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureProperty.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeatureProperty.java index e305f994c..359a5b9c0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureProperty.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feature/StaticFeatureProperty.java @@ -1,11 +1,11 @@ -package mil.nga.giat.mage.sdk.datastore.staticfeature; +package mil.nga.giat.mage.database.model.feature; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.io.Serializable; -import mil.nga.giat.mage.sdk.datastore.Property; +import mil.nga.giat.mage.database.model.Property; @DatabaseTable(tableName = "staticfeature_properties") public class StaticFeatureProperty extends Property { diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/Feed.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/Feed.kt similarity index 97% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/Feed.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/Feed.kt index b8954224c..fb3f3e3e0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/Feed.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/Feed.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.* import com.google.gson.JsonElement diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedAndLocal.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedAndLocal.kt similarity index 69% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedAndLocal.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedAndLocal.kt index fbf27da11..6ccbb6a37 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedAndLocal.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedAndLocal.kt @@ -1,11 +1,11 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.Embedded import androidx.room.Relation data class FeedAndLocal( - @Embedded val feed: Feed, - @Relation( + @Embedded val feed: Feed, + @Relation( parentColumn = "id", entityColumn = "feed_id" ) diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedContent.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedContent.kt similarity index 51% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedContent.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedContent.kt index 745e3a0aa..0aec955cb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedContent.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedContent.kt @@ -1,11 +1,8 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.SerializedName -import com.mapbox.geojson.FeatureCollection -import mil.nga.giat.mage.data.gson.DateTimestampTypeAdapter -import mil.nga.giat.mage.data.gson.FeatureCollectionTypeAdapter -import mil.nga.sf.GeometryType +import mil.nga.giat.mage.network.gson.FeatureCollectionTypeAdapter data class FeedContent( diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItem.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItem.kt similarity index 91% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItem.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItem.kt index 84a2e4196..c53ff5b6d 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItem.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItem.kt @@ -1,9 +1,8 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey -import androidx.room.ForeignKey.CASCADE import com.google.gson.JsonElement import com.google.gson.annotations.SerializedName import mil.nga.sf.Geometry @@ -14,7 +13,7 @@ import mil.nga.sf.Geometry ForeignKey(entity = Feed::class, parentColumns = ["id"], childColumns = ["feed_id"], - onDelete = CASCADE) + onDelete = ForeignKey.CASCADE) ] ) data class FeedItem( @@ -43,7 +42,7 @@ data class FeedItem( ForeignKey(entity = Feed::class, parentColumns = ["id"], childColumns = ["feed_id"], - onDelete = CASCADE) + onDelete = ForeignKey.CASCADE) ] ) data class MappableFeedItem( diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemWithStyle.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItemWithStyle.kt similarity index 62% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemWithStyle.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItemWithStyle.kt index fe41d9d4e..11a1cc595 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedItemWithStyle.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedItemWithStyle.kt @@ -1,11 +1,11 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.* data class FeedItemWithStyle( - @Embedded + @Embedded val feedItem: FeedItem, - @Embedded + @Embedded val mapStyle: MapStyle ) \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocal.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedLocal.kt similarity index 76% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocal.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedLocal.kt index c477d5357..4592a298a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/FeedLocal.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedLocal.kt @@ -1,8 +1,7 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.* -import com.google.gson.JsonElement -import com.google.gson.annotations.SerializedName +import mil.nga.giat.mage.database.model.feed.Feed @Entity(tableName = "feed_local", foreignKeys = [ diff --git a/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedWithItems.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedWithItems.kt new file mode 100644 index 000000000..df5610ba5 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/FeedWithItems.kt @@ -0,0 +1,16 @@ +package mil.nga.giat.mage.database.model.feed + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Relation +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedItem + +@Entity +data class FeedWithItems( + @Embedded + val feed: Feed, + + @Relation(parentColumn = "id", entityColumn = "feed_id") + val items: List +) \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/database/model/feed/ItemWithFeed.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/ItemWithFeed.kt new file mode 100644 index 000000000..abae2de2e --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/ItemWithFeed.kt @@ -0,0 +1,16 @@ +package mil.nga.giat.mage.database.model.feed + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Relation +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedItem + +@Entity +data class ItemWithFeed( + @Relation(parentColumn = "item_feed_id", entityColumn = "id") + val feed: Feed, + + @Embedded(prefix = "item_") + val item: FeedItem +) \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/data/feed/MapStyle.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/MapStyle.kt similarity index 96% rename from mage/src/main/java/mil/nga/giat/mage/data/feed/MapStyle.kt rename to mage/src/main/java/mil/nga/giat/mage/database/model/feed/MapStyle.kt index eecba63e9..c0309cffb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/feed/MapStyle.kt +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/feed/MapStyle.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.feed +package mil.nga.giat.mage.database.model.feed import androidx.room.ColumnInfo import androidx.room.Embedded diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/Layer.java b/mage/src/main/java/mil/nga/giat/mage/database/model/layer/Layer.java similarity index 96% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/Layer.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/layer/Layer.java index 961c361c9..be9971209 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/Layer.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/layer/Layer.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.layer; +package mil.nga.giat.mage.database.model.layer; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.field.ForeignCollectionField; @@ -11,8 +11,8 @@ import java.util.ArrayList; import java.util.Collection; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.datastore.user.Event; +import mil.nga.giat.mage.database.model.feature.StaticFeature; +import mil.nga.giat.mage.database.model.event.Event; @DatabaseTable(tableName = "layers") public class Layer implements Comparable { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/Location.java b/mage/src/main/java/mil/nga/giat/mage/database/model/location/Location.java similarity index 92% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/Location.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/location/Location.java index d15c079c4..073cec18c 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/Location.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/location/Location.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.location; +package mil.nga.giat.mage.database.model.location; import androidx.annotation.NonNull; @@ -18,9 +18,9 @@ import java.util.Map; import mil.nga.giat.mage.sdk.Temporal; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.utils.GeometryUtility; +import mil.nga.giat.mage.database.model.event.Event; +import mil.nga.giat.mage.database.model.user.User; +import mil.nga.giat.mage.sdk.utils.GeometryUtilityKt; import mil.nga.sf.Geometry; @DatabaseTable(tableName = "locations") @@ -79,7 +79,7 @@ public Location(String remoteId, User user, Date lastModified, String type, Coll this.lastModified = lastModified; this.type = type; this.properties = properties; - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); this.timestamp = timestamp; this.event = event; } @@ -150,11 +150,11 @@ public void setGeometryBytes(byte[] geometryBytes) { } public Geometry getGeometry() { - return GeometryUtility.toGeometry(getGeometryBytes()); + return GeometryUtilityKt.toGeometry(getGeometryBytes()); } public void setGeometry(Geometry geometry) { - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); } /** diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationProperty.java b/mage/src/main/java/mil/nga/giat/mage/database/model/location/LocationProperty.java similarity index 85% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationProperty.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/location/LocationProperty.java index 92d6f59ef..9323ecb34 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationProperty.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/location/LocationProperty.java @@ -1,11 +1,11 @@ -package mil.nga.giat.mage.sdk.datastore.location; +package mil.nga.giat.mage.database.model.location; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.io.Serializable; -import mil.nga.giat.mage.sdk.datastore.Property; +import mil.nga.giat.mage.database.model.Property; @DatabaseTable(tableName = "location_properties") public class LocationProperty extends Property { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Attachment.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/Attachment.java similarity index 98% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Attachment.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/Attachment.java index e58f73130..99d8283e0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Attachment.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/Attachment.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import android.os.Parcel; import android.os.Parcelable; @@ -47,7 +47,7 @@ public class Attachment implements Parcelable, Serializable { private String url; @DatabaseField(canBeNull = false) - private boolean dirty = Boolean.TRUE; + private boolean dirty = true; @DatabaseField(canBeNull = false, foreign = true, foreignAutoRefresh = true) private transient Observation observation; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Observation.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/Observation.java similarity index 94% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Observation.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/Observation.java index 88ab1a08f..8808a59d0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/Observation.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/Observation.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,8 +19,8 @@ import java.util.Map; import mil.nga.giat.mage.sdk.Temporal; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.utils.GeometryUtility; +import mil.nga.giat.mage.database.model.event.Event; +import mil.nga.giat.mage.sdk.utils.GeometryUtilityKt; import mil.nga.sf.Geometry; @DatabaseTable(tableName = "observations") @@ -58,7 +58,7 @@ public class Observation implements Comparable, Temporal { private Date timestamp = new Date(0); @DatabaseField(canBeNull = false) - private boolean dirty = Boolean.TRUE; + private boolean dirty = true; @DatabaseField(canBeNull = false) private State state = State.ACTIVE; @@ -87,7 +87,7 @@ public class Observation implements Comparable, Temporal { @DatabaseField(canBeNull = true, foreign = true, foreignAutoRefresh = true, foreignAutoCreate = true) private ObservationImportant important; - @DatabaseField(persisterClass = ObservationErrorClassPersister.class, columnName = "error") + @DatabaseField(persisterClass = ObservationErrorPersister.class, columnName = "error") private ObservationError error; @ForeignCollectionField(eager = true) @@ -106,7 +106,7 @@ public Observation(String remoteId, Date lastModified, Geometry geometry, Collec super(); this.remoteId = remoteId; this.lastModified = lastModified; - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); this.forms = forms; this.attachments = attachments; this.dirty = false; @@ -171,11 +171,11 @@ public void setGeometryBytes(byte[] geometryBytes) { } public Geometry getGeometry() { - return GeometryUtility.toGeometry(getGeometryBytes()); + return GeometryUtilityKt.toGeometry(getGeometryBytes()); } public void setGeometry(Geometry geometry) { - this.geometryBytes = GeometryUtility.toGeometryBytes(geometry); + this.geometryBytes = GeometryUtilityKt.toBytes(geometry); } public String getProvider() { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationError.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationError.java similarity index 91% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationError.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationError.java index 03e533aab..cf6cccc7d 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationError.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationError.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; public class ObservationError { diff --git a/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationErrorPersister.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationErrorPersister.kt new file mode 100644 index 000000000..48b17b0dc --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationErrorPersister.kt @@ -0,0 +1,74 @@ +package mil.nga.giat.mage.database.model.observation + +import android.util.Log +import com.j256.ormlite.field.FieldType +import com.j256.ormlite.field.SqlType +import com.j256.ormlite.field.types.StringType +import org.json.JSONException +import org.json.JSONObject + +class ObservationErrorPersister private constructor() : StringType( + SqlType.STRING, arrayOf(ObservationErrorPersister::class.java) +) { + override fun javaToSqlArg(fieldType: FieldType, javaObject: Any): Any { + val error = javaObject as ObservationError + return jsonFromError(error) + } + + override fun sqlArgToJava(fieldType: FieldType, sqlArg: Any, columnPos: Int): Any { + val json = sqlArg as String + return errorFromJson(json) + } + + private fun jsonFromError(error: ObservationError): String { + val jsonObject = JSONObject() + try { + val statusCode = error.statusCode + if (statusCode != null) { + jsonObject.put(ERROR_STATUS_CODE_KEY, error.statusCode) + } + val message = error.message + if (message != null) { + jsonObject.put(ERROR_MESSAGE_KEY, error.message) + } + val description = error.description + if (description != null) { + jsonObject.put(ERROR_DESCRIPTION_KEY, error.description) + } + } catch (e: JSONException) { + Log.i(LOG_NAME, "Error parsing observation error", e) + } + + return jsonObject.toString() + } + + private fun errorFromJson(errorJson: String): ObservationError { + val observationError = ObservationError() + try { + val jsonObject = JSONObject(errorJson) + if (jsonObject.has(ERROR_STATUS_CODE_KEY)) { + observationError.statusCode = jsonObject.getInt(ERROR_STATUS_CODE_KEY) + } + if (jsonObject.has(ERROR_MESSAGE_KEY)) { + observationError.message = jsonObject.getString(ERROR_MESSAGE_KEY) + } + if (jsonObject.has(ERROR_DESCRIPTION_KEY)) { + observationError.description = jsonObject.getString(ERROR_DESCRIPTION_KEY) + } + } catch (e: JSONException) { + Log.e(LOG_NAME, "Error parsing json into observation error class", e) + } + return observationError + } + + companion object { + private val LOG_NAME = ObservationErrorPersister::class.java.name + + private const val ERROR_STATUS_CODE_KEY = "ERROR_STATUS_CODE_KEY" + private const val ERROR_MESSAGE_KEY = "ERROR_MESSAGE_KEY" + private const val ERROR_DESCRIPTION_KEY = "ERROR_DESCRIPTION_KEY" + + @JvmStatic + val singleton = ObservationErrorPersister() + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationFavorite.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationFavorite.java similarity index 91% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationFavorite.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationFavorite.java index 834d47ff5..cbd2fff85 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationFavorite.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationFavorite.java @@ -1,10 +1,12 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import org.apache.commons.lang3.builder.ToStringBuilder; +import mil.nga.giat.mage.database.model.observation.Observation; + @DatabaseTable(tableName = "observation_favorites") public class ObservationFavorite { @@ -18,7 +20,7 @@ public class ObservationFavorite { private boolean favorite; @DatabaseField(canBeNull = false, columnName = "dirty") - private boolean dirty = Boolean.TRUE; + private boolean dirty = true; @DatabaseField(canBeNull = false, foreign = true, foreignAutoRefresh = true) private Observation observation; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationForm.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationForm.java similarity index 97% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationForm.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationForm.java index 27c7854b8..894846143 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationForm.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationForm.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.field.ForeignCollectionField; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationImportant.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationImportant.java similarity index 95% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationImportant.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationImportant.java index 8615669d8..24f9c17de 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationImportant.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationImportant.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; @@ -27,7 +27,7 @@ public class ObservationImportant { private boolean important; @DatabaseField(canBeNull = false, columnName = "dirty") - private boolean dirty = Boolean.TRUE; + private boolean dirty = true; public ObservationImportant() { // ORMLite needs a no-arg constructor diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationProperty.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationProperty.java similarity index 90% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationProperty.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationProperty.java index d3304c3cd..3262ffdd1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationProperty.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/ObservationProperty.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.observation; +package mil.nga.giat.mage.database.model.observation; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; @@ -8,7 +8,7 @@ import java.io.Serializable; import java.util.ArrayList; -import mil.nga.giat.mage.sdk.datastore.Property; +import mil.nga.giat.mage.database.model.Property; @DatabaseTable(tableName = "observation_properties") public class ObservationProperty extends Property { diff --git a/mage/src/main/java/mil/nga/giat/mage/database/model/observation/State.java b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/State.java new file mode 100644 index 000000000..cee6c824e --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/observation/State.java @@ -0,0 +1,10 @@ +package mil.nga.giat.mage.database.model.observation; + +import mil.nga.giat.mage.database.model.observation.Observation; + +/** + * The State of an {@link Observation} + */ +public enum State { + ACTIVE, COMPLETE, ARCHIVE +} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permission.java b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permission.java similarity index 81% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permission.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permission.java index e70439c27..df0e88778 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permission.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permission.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.permission; import java.io.Serializable; @@ -19,6 +19,13 @@ public enum Permission implements Serializable { DELETE_ROLE, DELETE_TEAM, DELETE_USER, + FEEDS_LIST_SERVICE_TYPES, + FEEDS_CREATE_SERVICE, + FEEDS_LIST_SERVICES, + FEEDS_LIST_TOPICS, + FEEDS_CREATE_FEED, + FEEDS_LIST_ALL, + FEEDS_FETCH_CONTENT, READ_DEVICE, READ_EVENT_ALL, READ_EVENT_USER, diff --git a/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permissions.kt b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permissions.kt new file mode 100644 index 000000000..e8ac19342 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Permissions.kt @@ -0,0 +1,17 @@ +package mil.nga.giat.mage.database.model.permission + +import java.io.Serializable + +class Permissions : Serializable { + var permissions = emptyList() + + constructor() + + constructor(permissions: List) : super() { + this.permissions = permissions + } + + companion object { + private const val serialVersionUID = -1912604919150929355L + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Role.java b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Role.java similarity index 97% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Role.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/permission/Role.java index 32e0b48dc..33a5c06b6 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Role.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/permission/Role.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.permission; import com.j256.ormlite.field.DataType; import com.j256.ormlite.field.DatabaseField; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Team.java b/mage/src/main/java/mil/nga/giat/mage/database/model/team/Team.java similarity index 95% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Team.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/team/Team.java index 25f451d73..fa016fe07 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Team.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/team/Team.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.team; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; @@ -9,6 +9,8 @@ import java.util.ArrayList; import java.util.Collection; +import mil.nga.giat.mage.database.model.user.User; + @DatabaseTable(tableName = "teams") public class Team { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamEvent.java b/mage/src/main/java/mil/nga/giat/mage/database/model/team/TeamEvent.java similarity index 87% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamEvent.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/team/TeamEvent.java index 136750a8d..b2ff1b0eb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamEvent.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/team/TeamEvent.java @@ -1,8 +1,10 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.team; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import mil.nga.giat.mage.database.model.event.Event; + /** * Join table for the many to many relationship between teams and events */ diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Phone.java b/mage/src/main/java/mil/nga/giat/mage/database/model/user/Phone.java similarity index 86% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Phone.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/user/Phone.java index e8a55a6e8..b2e98d44b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Phone.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/user/Phone.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.user; import java.io.Serializable; diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/User.java b/mage/src/main/java/mil/nga/giat/mage/database/model/user/User.java similarity index 96% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/User.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/user/User.java index 3cea49f98..5b80a0fc0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/User.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/user/User.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.user; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; @@ -11,6 +11,9 @@ import java.util.Date; import java.util.List; +import mil.nga.giat.mage.database.model.event.Event; +import mil.nga.giat.mage.database.model.permission.Role; + @DatabaseTable(tableName = "users") public class User { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserLocal.java b/mage/src/main/java/mil/nga/giat/mage/database/model/user/UserLocal.java similarity index 96% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserLocal.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/user/UserLocal.java index 3ba119d26..49f0ea943 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserLocal.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/user/UserLocal.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.user; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; @@ -6,6 +6,8 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; +import mil.nga.giat.mage.database.model.event.Event; + @DatabaseTable(tableName = "userlocal") public class UserLocal { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserTeam.java b/mage/src/main/java/mil/nga/giat/mage/database/model/user/UserTeam.java similarity index 87% rename from mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserTeam.java rename to mage/src/main/java/mil/nga/giat/mage/database/model/user/UserTeam.java index 0a8f16384..360a18274 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserTeam.java +++ b/mage/src/main/java/mil/nga/giat/mage/database/model/user/UserTeam.java @@ -1,8 +1,10 @@ -package mil.nga.giat.mage.sdk.datastore.user; +package mil.nga.giat.mage.database.model.user; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; +import mil.nga.giat.mage.database.model.team.Team; + /** * Join table for the many to many relationship between users and teams */ diff --git a/mage/src/main/java/mil/nga/giat/mage/di/NetworkModule.kt b/mage/src/main/java/mil/nga/giat/mage/di/NetworkModule.kt index 4fb3373ed..861607d6a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/di/NetworkModule.kt +++ b/mage/src/main/java/mil/nga/giat/mage/di/NetworkModule.kt @@ -1,7 +1,8 @@ package mil.nga.giat.mage.di -import android.app.Application +import LocationsTypeAdapter import android.content.Context +import android.util.Log import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken @@ -10,50 +11,115 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import mil.nga.giat.mage.data.gson.AnnotationExclusionStrategy -import mil.nga.giat.mage.data.gson.DateTimestampTypeAdapter -import mil.nga.giat.mage.data.gson.GeometryTypeAdapterFactory +import mil.nga.giat.mage.network.gson.AnnotationExclusionStrategy +import mil.nga.giat.mage.network.gson.DateTimestampTypeAdapter +import mil.nga.giat.mage.network.gson.GeometryTypeAdapterFactory import mil.nga.giat.mage.network.LiveDataCallAdapterFactory import mil.nga.giat.mage.network.Server import mil.nga.giat.mage.network.api.* -import mil.nga.giat.mage.network.gson.LocationsTypeAdapter -import mil.nga.giat.mage.network.gson.observation.AttachmentTypeAdapter -import mil.nga.giat.mage.network.gson.observation.ObservationTypeAdapter -import mil.nga.giat.mage.network.gson.observation.ObservationsTypeAdapter -import mil.nga.giat.mage.network.gson.user.UserTypeAdapter -import mil.nga.giat.mage.sdk.datastore.layer.Layer -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.Role -import mil.nga.giat.mage.sdk.datastore.user.Team -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.gson.deserializer.EventsDeserializer -import mil.nga.giat.mage.sdk.gson.deserializer.LayersDeserializer -import mil.nga.giat.mage.sdk.gson.deserializer.RolesDeserializer -import mil.nga.giat.mage.sdk.gson.deserializer.TeamsDeserializer -import mil.nga.giat.mage.sdk.http.HttpClientManager +import mil.nga.giat.mage.network.attachment.AttachmentTypeAdapter +import mil.nga.giat.mage.network.observation.ObservationTypeAdapter +import mil.nga.giat.mage.network.observation.ObservationsTypeAdapter +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.network.attachment.AttachmentService +import mil.nga.giat.mage.network.attachment.AttachmentService_server5 +import mil.nga.giat.mage.network.device.DeviceService +import mil.nga.giat.mage.network.event.EventService +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.permission.Role +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.network.event.EventsDeserializer +import mil.nga.giat.mage.network.feed.FeedService +import mil.nga.giat.mage.network.layer.LayersDeserializer +import mil.nga.giat.mage.network.role.RolesDeserializer +import mil.nga.giat.mage.network.team.TeamsDeserializer +import mil.nga.giat.mage.network.layer.LayerService +import mil.nga.giat.mage.network.location.LocationService +import mil.nga.giat.mage.network.location.UserLocations +import mil.nga.giat.mage.network.location.UserLocationsTypeAdapter +import mil.nga.giat.mage.network.observation.ObservationService +import mil.nga.giat.mage.network.role.RoleService +import mil.nga.giat.mage.network.team.TeamService +import mil.nga.giat.mage.network.user.UserService +import mil.nga.giat.mage.network.user.UserWithRole +import mil.nga.giat.mage.network.user.UserWithRoleId +import mil.nga.giat.mage.network.user.UserWithRoleIdTypeAdapter +import mil.nga.giat.mage.network.user.UserWithRoleTypeAdapter +import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.net.HttpURLConnection import java.util.* +import java.util.concurrent.TimeUnit import javax.inject.Qualifier import javax.inject.Singleton @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class server5 +annotation class Server5 + +data class UserAgentHeader( + val name: String = "User-Agent", + val value: String +) @InstallIn(SingletonComponent::class) @Module class NetworkModule { + @Singleton @Provides + fun provideUserAgent(): UserAgentHeader { + val value = System.getProperty("http.agent") ?: "Unknown Android Http Agent" + return UserAgentHeader(value = value) + } + @Singleton - fun provideHttpClient(): OkHttpClient { - val builder = HttpClientManager.getInstance().httpClient().newBuilder() - return builder.build() + @Provides + fun provideHttpTokenInterceptor( + tokenProvider: TokenProvider, + userAgentHeader: UserAgentHeader + ): Interceptor { + val nonTokenRoutes = listOf("/auth/token", "/api/users/myself/password") + return Interceptor { chain -> + val builder = chain.request().newBuilder() + + if (!nonTokenRoutes.contains(chain.request().url.encodedPath)) { + tokenProvider.value?.let { tokenStatus -> + if (tokenStatus is TokenStatus.Active) { + builder.addHeader("Authorization", "Bearer ${tokenStatus.token.token}") + } + } + } + + builder.addHeader(userAgentHeader.name, userAgentHeader.value) + val response = chain.proceed(builder.build()) + val statusCode = response.code + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + Log.d(LOG_NAME, "Token expired") + tokenProvider.expireToken() + } else if (statusCode == HttpURLConnection.HTTP_NOT_FOUND) { + Log.w(LOG_NAME, response.message) + } + response + } + } + + @Singleton + @Provides + fun provideOkHttpClient( + tokenInterceptor: Interceptor + ): OkHttpClient { + return OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .addInterceptor(tokenInterceptor) + .build() } @Provides @@ -64,18 +130,20 @@ class NetworkModule { @Provides @Singleton - fun provideGson(application: Application): Gson { + fun provideGson(): Gson { return GsonBuilder() .setExclusionStrategies(AnnotationExclusionStrategy()) - .registerTypeAdapter(object : TypeToken() {}.type, UserTypeAdapter(application)) + .registerTypeAdapter(object : TypeToken() {}.type, UserWithRoleTypeAdapter()) + .registerTypeAdapter(object : TypeToken() {}.type, UserWithRoleIdTypeAdapter()) .registerTypeAdapter(object : TypeToken() {}.type, ObservationTypeAdapter()) .registerTypeAdapter(object : TypeToken() {}.type, AttachmentTypeAdapter()) - .registerTypeAdapter(object : TypeToken>() {}.type, RolesDeserializer()) - .registerTypeAdapter(object : TypeToken>() {}.type, LayersDeserializer()) + .registerTypeAdapter(object : TypeToken>() {}.type, RolesDeserializer()) + .registerTypeAdapter(object : TypeToken>() {}.type, LayersDeserializer()) .registerTypeAdapter(object : TypeToken>() {}.type, LocationsTypeAdapter()) + .registerTypeAdapter(object : TypeToken>() {}.type, UserLocationsTypeAdapter()) .registerTypeAdapter(object : TypeToken>() {}.type, ObservationsTypeAdapter()) - .registerTypeAdapter(object : TypeToken>>() {}.type, TeamsDeserializer(application)) - .registerTypeAdapter(object : TypeToken>>() {}.type, EventsDeserializer(application)) + .registerTypeAdapter(object : TypeToken>>() {}.type, TeamsDeserializer()) + .registerTypeAdapter(object : TypeToken>() {}.type, EventsDeserializer()) .registerTypeAdapterFactory(GeometryTypeAdapterFactory()) .registerTypeAdapter(Date::class.java, DateTimestampTypeAdapter()) .create() @@ -95,6 +163,11 @@ class NetworkModule { .build() } + @Provides + fun provideApiService(retrofit: Retrofit): ApiService { + return retrofit.create(ApiService::class.java) + } + @Provides fun provideRoleService(retrofit: Retrofit): RoleService { return retrofit.create(RoleService::class.java) @@ -120,6 +193,11 @@ class NetworkModule { return retrofit.create(UserService::class.java) } + @Provides + fun provideDeviceService(retrofit: Retrofit): DeviceService { + return retrofit.create(DeviceService::class.java) + } + @Provides fun provideObservationService(retrofit: Retrofit): ObservationService { return retrofit.create(ObservationService::class.java) @@ -131,7 +209,7 @@ class NetworkModule { } @Provides - @server5 + @Server5 fun provideAttachmentService_server5(retrofit: Retrofit): AttachmentService_server5 { return retrofit.create(AttachmentService_server5::class.java) } @@ -145,4 +223,8 @@ class NetworkModule { fun provideFeedService(retrofit: Retrofit): FeedService { return retrofit.create(FeedService::class.java) } + + companion object { + private val LOG_NAME = NetworkModule::class.java.name + } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/di/OrmLiteModule.kt b/mage/src/main/java/mil/nga/giat/mage/di/OrmLiteModule.kt new file mode 100644 index 000000000..086362e20 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/di/OrmLiteModule.kt @@ -0,0 +1,156 @@ +package mil.nga.giat.mage.di + +import android.app.Application +import com.j256.ormlite.android.apptools.OpenHelperManager +import com.j256.ormlite.dao.Dao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.feature.StaticFeatureProperty +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.location.LocationProperty +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.permission.Role +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.database.model.team.TeamEvent +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.database.model.user.UserLocal +import mil.nga.giat.mage.database.model.user.UserTeam +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +class OrmLiteModule { + + @Provides + @Singleton + fun provideSQLiteOpenHelper(application: Application): MageSqliteOpenHelper { + OpenHelperManager.setOpenHelperClass(MageSqliteOpenHelper::class.java) + return OpenHelperManager.getHelper(application, MageSqliteOpenHelper::class.java) + } + + @Provides + @Singleton + fun provideUserDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(User::class.java) + } + + @Provides + @Singleton + fun provideUserLocalDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(UserLocal::class.java) + } + + @Provides + @Singleton + fun provideRoleDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Role::class.java) + } + + @Provides + @Singleton + fun provideTeamDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Team::class.java) + } + + @Provides + @Singleton + fun provideUserTeamDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(UserTeam::class.java) + } + + @Provides + @Singleton + fun provideEventDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Event::class.java) + } + + @Provides + @Singleton + fun provideTeamEventDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(TeamEvent::class.java) + } + + @Provides + @Singleton + fun provideFormDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Form::class.java) + } + + @Provides + @Singleton + fun provideLocationDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Location::class.java) + } + + @Provides + @Singleton + fun provideLocationPropertyDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(LocationProperty::class.java) + } + + @Provides + @Singleton + fun provideObservationDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Observation::class.java) + } + + @Provides + @Singleton + fun provideObservationFormDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(ObservationForm::class.java) + } + + @Provides + @Singleton + fun provideObservationPropertyDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(ObservationProperty::class.java) + } + + @Provides + @Singleton + fun provideObservationImportantDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(ObservationImportant::class.java) + } + + @Provides + @Singleton + fun provideObservationFavoriteDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(ObservationFavorite::class.java) + } + + @Provides + @Singleton + fun provideAttachmentDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Attachment::class.java) + } + + @Provides + @Singleton + fun provideLayerDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(Layer::class.java) + } + + @Provides + @Singleton + fun provideFeatureDaoDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(StaticFeature::class.java) + } + + @Provides + @Singleton + fun provideFeaturePropertyDaoDao(daoStore: MageSqliteOpenHelper): Dao { + return daoStore.getDao(StaticFeatureProperty::class.java) + } +} diff --git a/mage/src/main/java/mil/nga/giat/mage/di/RoomModule.kt b/mage/src/main/java/mil/nga/giat/mage/di/RoomModule.kt index b4d41488f..0c3bbeef5 100644 --- a/mage/src/main/java/mil/nga/giat/mage/di/RoomModule.kt +++ b/mage/src/main/java/mil/nga/giat/mage/di/RoomModule.kt @@ -1,17 +1,15 @@ package mil.nga.giat.mage.dagger.module import android.app.Application -import android.content.Context import androidx.room.Room import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import mil.nga.giat.mage.data.MageDatabase -import mil.nga.giat.mage.data.feed.FeedDao -import mil.nga.giat.mage.data.feed.FeedItemDao -import mil.nga.giat.mage.data.feed.FeedLocalDao +import mil.nga.giat.mage.database.MageDatabase +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.dao.feed.FeedItemDao +import mil.nga.giat.mage.database.dao.feed.FeedLocalDao import javax.inject.Singleton @InstallIn(SingletonComponent::class) diff --git a/mage/src/main/java/mil/nga/giat/mage/di/TokenProvider.kt b/mage/src/main/java/mil/nga/giat/mage/di/TokenProvider.kt new file mode 100644 index 000000000..ce4946509 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/di/TokenProvider.kt @@ -0,0 +1,106 @@ +package mil.nga.giat.mage.di + +import android.app.Application +import android.content.SharedPreferences +import androidx.lifecycle.LiveData +import mil.nga.giat.mage.R +import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import java.util.Date +import javax.inject.Inject +import javax.inject.Singleton + +data class Token( + val username: String, + val token: String, + val expirationDate: Date +) { + fun isExpired(): Boolean { + return Date().after(expirationDate) + } +} + +sealed class TokenStatus { + data class Active(val token: Token): TokenStatus() + object Expired: TokenStatus() + object Logout: TokenStatus() +} + +@Singleton +class TokenProvider @Inject constructor ( + private val application: Application, + private val preferences: SharedPreferences +): LiveData() { + + init { + val username = preferences.getString(application.getString(R.string.sessionUserKey), "")!! + val tokenValue = preferences.getString(application.getString(R.string.tokenKey), "")!! + val expiration = preferences.getString(application.getString(R.string.tokenExpirationDateKey), "")!! + val expirationDate = + try { + ISO8601DateFormatFactory.ISO8601().parse(expiration) + } catch (_: Exception) { + Date() + } + + val token = Token( + token = tokenValue, + expirationDate = expirationDate, + username = username + ) + + postValue(TokenStatus.Active(token)) + } + + fun updateToken( + username: String, + authenticationStrategy: String, + token: String, + expiration: String + ) { + val expirationDate = ISO8601DateFormatFactory.ISO8601().parse(expiration)!! + + preferences.edit() + .putString(application.getString(R.string.sessionUserKey), username) + .putString(application.getString(R.string.sessionStrategyKey), authenticationStrategy) + .putString(application.getString(R.string.tokenKey), token) + .putString(application.getString(R.string.tokenExpirationDateKey), expiration) + .putLong(application.getString(R.string.tokenExpirationLengthKey), expirationDate.time - Date().time) + .apply() + + val status = TokenStatus.Active( + Token( + username = username, + token = token, + expirationDate = expirationDate + ) + ) + + postValue(status) + } + + fun signout() { + removeToken() + postValue(TokenStatus.Logout) + } + + fun expireToken() { + removeToken() + postValue(TokenStatus.Expired) + } + + private fun removeToken() { + preferences.edit() + .remove(application.getString(R.string.tokenKey)) + .remove(application.getString(R.string.tokenExpirationDateKey)) + .apply() + } + + fun isExpired(): Boolean { + return value?.let { tokenStatus -> + when (tokenStatus) { + is TokenStatus.Active -> tokenStatus.token.isExpired() + else -> true + } + } ?: true + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.java b/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.java deleted file mode 100644 index 1b7803ed3..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.java +++ /dev/null @@ -1,85 +0,0 @@ -package mil.nga.giat.mage.disclaimer; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import mil.nga.giat.mage.MageApplication; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.event.EventsActivity; -import mil.nga.giat.mage.login.LoginActivity; - -@AndroidEntryPoint -public class DisclaimerActivity extends AppCompatActivity { - - @Inject - protected MageApplication application; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - requestWindowFeature(Window.FEATURE_NO_TITLE); - - setContentView(R.layout.activity_disclaimer); - getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); - } - - @Override - protected void onResume() { - super.onResume(); - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - - boolean showDisclaimer = sharedPreferences.getBoolean(getString(R.string.serverDisclaimerShow), false); - - if (showDisclaimer) { - String disclaimerTitle = sharedPreferences.getString(getString(R.string.serverDisclaimerTitle), null); - TextView disclaimerTitleView = findViewById(R.id.disclaimer_title); - disclaimerTitleView.setText(disclaimerTitle); - - TextView disclaimerTextView = findViewById(R.id.disclaimer_text); - String disclaimerText = sharedPreferences.getString(getString(R.string.serverDisclaimerText), null); - disclaimerTextView.setText(disclaimerText); - } else { - agree(null); - } - } - - public void agree(View view) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - sharedPreferences.edit().putBoolean(getString(R.string.disclaimerAcceptedKey), true).apply(); - - Intent intent = new Intent(getApplicationContext(), EventsActivity.class); - Bundle extras = getIntent().getExtras(); - if (extras != null) { - intent.putExtras(extras); - } - - startActivity(intent); - finish(); - } - - public void exit(View view) { - application.onLogout(true, new MageApplication.OnLogoutListener() { - @Override - public void onLogout() { - startActivity(new Intent(getApplicationContext(), LoginActivity.class)); - finish(); - } - }); - } - - @Override - public void onBackPressed() { - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.kt b/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.kt new file mode 100644 index 000000000..683a1920d --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/disclaimer/DisclaimerActivity.kt @@ -0,0 +1,68 @@ +package mil.nga.giat.mage.disclaimer + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import android.view.Window +import android.view.WindowManager +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.preference.PreferenceManager +import dagger.hilt.android.AndroidEntryPoint +import mil.nga.giat.mage.MageApplication +import mil.nga.giat.mage.R +import mil.nga.giat.mage.event.EventsActivity +import mil.nga.giat.mage.login.LoginActivity +import javax.inject.Inject + +@AndroidEntryPoint +class DisclaimerActivity : AppCompatActivity() { + @Inject lateinit var application: MageApplication + @Inject lateinit var preferences: SharedPreferences + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(R.layout.activity_disclaimer) + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) + } + + override fun onResume() { + super.onResume() + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext) + val showDisclaimer = sharedPreferences.getBoolean(getString(R.string.serverDisclaimerShow), false) + if (showDisclaimer) { + val disclaimerTitle = sharedPreferences.getString(getString(R.string.serverDisclaimerTitle), null) + val disclaimerTitleView = findViewById(R.id.disclaimer_title) + disclaimerTitleView.text = disclaimerTitle + val disclaimerTextView = findViewById(R.id.disclaimer_text) + val disclaimerText = sharedPreferences.getString(getString(R.string.serverDisclaimerText), null) + disclaimerTextView.text = disclaimerText + } else { + agree(null) + } + } + + fun agree(view: View?) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences( + applicationContext + ) + sharedPreferences.edit().putBoolean(getString(R.string.disclaimerAcceptedKey), true).apply() + val intent = Intent(applicationContext, EventsActivity::class.java) + val extras = getIntent().extras + if (extras != null) { + intent.putExtras(extras) + } + startActivity(intent) + finish() + } + + fun exit(view: View?) { + application.onLogout(true) + startActivity(Intent(applicationContext, LoginActivity::class.java)) + finish() + } + + override fun onBackPressed() {} +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/event/EventActivity.kt b/mage/src/main/java/mil/nga/giat/mage/event/EventActivity.kt index 486b761d0..efb1097f6 100644 --- a/mage/src/main/java/mil/nga/giat/mage/event/EventActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/event/EventActivity.kt @@ -1,7 +1,6 @@ package mil.nga.giat.mage.event import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.MenuItem import android.view.ViewGroup @@ -15,28 +14,21 @@ import mil.nga.giat.mage.databinding.ActivityEventBinding import mil.nga.giat.mage.databinding.RecyclerFormListItemBinding import mil.nga.giat.mage.form.Form import mil.nga.giat.mage.form.defaults.FormDefaultActivity -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.exceptions.EventException +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource import javax.inject.Inject @AndroidEntryPoint class EventActivity : AppCompatActivity() { - companion object { - private val LOG_NAME = EventActivity::class.java.name - - const val EVENT_ID_EXTRA = "EVENT_ID_EXTRA" - } - private lateinit var binding: ActivityEventBinding private lateinit var viewAdapter: RecyclerView.Adapter<*> private lateinit var viewManager: RecyclerView.LayoutManager var event: Event? = null - @Inject - lateinit var application: MageApplication + @Inject lateinit var application: MageApplication + @Inject lateinit var eventLocalDataSource: EventLocalDataSource public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -49,12 +41,8 @@ class EventActivity : AppCompatActivity() { setSupportActionBar(binding.toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) - val eventHelper: EventHelper = EventHelper.getInstance(applicationContext) - val eventId = intent.extras?.getLong(EVENT_ID_EXTRA) - try { - event = eventHelper.read(eventId) - } catch(e: EventException) { - Log.e(LOG_NAME, "Error reading event", e) + intent.extras?.getLong(EVENT_ID_EXTRA)?.let { eventId -> + event = eventLocalDataSource.read(eventId) } viewManager = LinearLayoutManager(this) @@ -111,4 +99,8 @@ class EventActivity : AppCompatActivity() { override fun getItemCount() = forms.count() } + + companion object { + const val EVENT_ID_EXTRA = "EVENT_ID_EXTRA" + } } diff --git a/mage/src/main/java/mil/nga/giat/mage/event/EventListAdapter.java b/mage/src/main/java/mil/nga/giat/mage/event/EventListAdapter.java index 412e7c590..94e0d880f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/event/EventListAdapter.java +++ b/mage/src/main/java/mil/nga/giat/mage/event/EventListAdapter.java @@ -19,7 +19,7 @@ import java.util.List; import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.user.Event; +import mil.nga.giat.mage.database.model.event.Event; /** * Created by wnewman on 2/23/18. diff --git a/mage/src/main/java/mil/nga/giat/mage/event/EventViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/event/EventViewModel.kt index 9baf4ff7d..9de475b60 100644 --- a/mage/src/main/java/mil/nga/giat/mage/event/EventViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/event/EventViewModel.kt @@ -1,50 +1,51 @@ package mil.nga.giat.mage.event -import android.content.Context import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import mil.nga.giat.mage.data.event.EventRepository +import mil.nga.giat.mage.data.repository.event.EventRepository import mil.nga.giat.mage.network.Resource -import mil.nga.giat.mage.network.api.UserService -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.network.user.UserService +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.data.repository.layer.LayerRepository import javax.inject.Inject @HiltViewModel class EventViewModel @Inject constructor( - @ApplicationContext val context: Context, - private val eventRepository: EventRepository, - private val userService: UserService + private val eventRepository: EventRepository, + private val userService: UserService, + private val userLocalDataSource: UserLocalDataSource, + private val layerRepository: LayerRepository ): ViewModel() { - val userHelper: UserHelper = UserHelper.getInstance(context) + val events: LiveData> = liveData { + val events = eventRepository.getEvents(forceUpdate = true) + emit(events) + } - val events: LiveData> = liveData { - val events = eventRepository.getEvents(forceUpdate = true) - emit(events) - } + private val _syncStatus = MutableLiveData>() + val syncStatus: LiveData> = _syncStatus + fun syncEvent(event: Event): LiveData> { + viewModelScope.launch(Dispatchers.IO) { + val result = eventRepository.syncEvent(event) + _syncStatus.postValue(result) + } - private val _syncStatus = MutableLiveData>() - val syncStatus: LiveData> = _syncStatus - fun syncEvent(event: Event): LiveData> { - viewModelScope.launch(Dispatchers.IO) { - val result = eventRepository.syncEvent(event) - _syncStatus.postValue(result) - } + return syncStatus + } - return syncStatus - } - - fun setEvent(event: Event) { - viewModelScope.launch(Dispatchers.IO) { - val user = userHelper.readCurrentUser() - userHelper.setCurrentEvent(user, event) + fun setEvent(event: Event) { + viewModelScope.launch(Dispatchers.IO) { + userLocalDataSource.readCurrentUser()?.let { user -> + userLocalDataSource.setCurrentEvent(user, event) + layerRepository.fetchFeatureLayers(event,false) + layerRepository.fetchImageryLayers() try { - userService.addRecentEvent(user.remoteId, event.remoteId) + userService.addRecentEvent(user.remoteId, event.remoteId) } catch(ignore: Exception) {} - } - } + } + } + } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/event/EventsActivity.kt b/mage/src/main/java/mil/nga/giat/mage/event/EventsActivity.kt index a7e542d33..815cfaf01 100644 --- a/mage/src/main/java/mil/nga/giat/mage/event/EventsActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/event/EventsActivity.kt @@ -2,7 +2,6 @@ package mil.nga.giat.mage.event import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.MenuItem import android.view.View import androidx.appcompat.app.AppCompatActivity @@ -17,15 +16,15 @@ import mil.nga.giat.mage.R import mil.nga.giat.mage.databinding.ActivityEventsBinding import mil.nga.giat.mage.login.LoginActivity import mil.nga.giat.mage.network.Resource -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource import javax.inject.Inject @AndroidEntryPoint class EventsActivity : AppCompatActivity() { - @Inject - lateinit var application: MageApplication + @Inject lateinit var application: MageApplication + @Inject lateinit var eventLocalDataSource: EventLocalDataSource private lateinit var binding: ActivityEventsBinding private lateinit var viewModel: EventViewModel @@ -51,18 +50,15 @@ class EventsActivity : AppCompatActivity() { binding.exit.setOnClickListener { dismiss() } - viewModel = ViewModelProvider(this).get(EventViewModel::class.java) + viewModel = ViewModelProvider(this)[EventViewModel::class.java] viewModel.syncStatus.observe(this) { onEventSynced(it) } // TODO what to do if user is not in this event // TODO all this should be in view model, either pick event and go or load events - var event: Event? = null - try { + val event = try { val eventId = intent.getLongExtra(EVENT_ID_EXTRA, -1) - event = EventHelper.getInstance(application).read(eventId) - } catch (e: java.lang.Exception) { - Log.e(LOG_NAME, "Could not read event", e) - } + eventLocalDataSource.read(eventId) + } catch (_: Exception) { null } if (event != null) { chooseEvent(event) @@ -85,7 +81,7 @@ class EventsActivity : AppCompatActivity() { when { events.size == 1 -> chooseEvent(events.first()) events.isNotEmpty() -> { - val recentEvents = EventHelper.getInstance(application).recentEvents + val recentEvents = eventLocalDataSource.getRecentEvents() val eventListAdapter = EventListAdapter(events.toMutableList(), recentEvents) { event -> chooseEvent(event) } binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -116,7 +112,7 @@ class EventsActivity : AppCompatActivity() { } private fun dismiss() { - application.onLogout(true, null) + application.onLogout(true) startActivity(Intent(applicationContext, LoginActivity::class.java)) finish() } @@ -146,8 +142,6 @@ class EventsActivity : AppCompatActivity() { } companion object { - private val LOG_NAME = EventsActivity::class.java.name - @JvmStatic val EVENT_ID_EXTRA = "EVENT_ID_EXTRA" @JvmStatic val CLOSABLE_EXTRA = "CLOSABLE_EXTRA" } diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/FeedActivity.kt b/mage/src/main/java/mil/nga/giat/mage/feed/FeedActivity.kt index 566a6c772..1a840be50 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/FeedActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/FeedActivity.kt @@ -11,7 +11,7 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.feed.Feed +import mil.nga.giat.mage.database.model.feed.Feed import mil.nga.giat.mage.feed.item.FeedItemActivity import mil.nga.giat.mage.utils.googleMapsUri diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/FeedFetchService.kt b/mage/src/main/java/mil/nga/giat/mage/feed/FeedFetchService.kt index b6b8ee43d..a629e3256 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/FeedFetchService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/FeedFetchService.kt @@ -5,36 +5,33 @@ import android.os.IBinder import android.util.Log import androidx.lifecycle.LifecycleService import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.switchMap import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* -import mil.nga.giat.mage.data.feed.Feed -import mil.nga.giat.mage.data.feed.FeedDao -import mil.nga.giat.mage.data.feed.FeedLocalDao -import mil.nga.giat.mage.data.feed.FeedRepository -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.dao.feed.FeedLocalDao +import mil.nga.giat.mage.data.repository.feed.FeedRepository +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.event.IEventEventListener import java.util.* import javax.inject.Inject @AndroidEntryPoint -class FeedFetchService : LifecycleService() { +class FeedFetchService: LifecycleService() { companion object { private val LOG_NAME = FeedFetchService::class.java.name private const val MIN_FETCH_DELAY = 5L } - @Inject - lateinit var feedDao: FeedDao - - @Inject - lateinit var feedLocalDao: FeedLocalDao - - @Inject - lateinit var feedRepository: FeedRepository + @Inject lateinit var feedDao: FeedDao + @Inject lateinit var feedLocalDao: FeedLocalDao + @Inject lateinit var feedRepository: FeedRepository + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource private val eventId = MutableLiveData() private var polling = false @@ -43,7 +40,7 @@ class FeedFetchService : LifecycleService() { override fun onCreate() { super.onCreate() - UserHelper.getInstance(applicationContext).addListener(object: IEventEventListener { + userLocalDataSource.addListener(object: IEventEventListener { override fun onEventChanged() { lifecycleScope.launch(Dispatchers.Main) { setEvent() @@ -64,7 +61,7 @@ class FeedFetchService : LifecycleService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - Transformations.switchMap(eventId) { eventId -> + eventId.switchMap { eventId -> stopPoll() eventId?.let { feedDao.feedsLiveData(it) @@ -88,8 +85,7 @@ class FeedFetchService : LifecycleService() { } private fun setEvent() { - val event = EventHelper.getInstance(applicationContext).currentEvent - eventId.value = event?.remoteId + eventId.value = eventLocalDataSource.currentEvent?.remoteId } private fun startPoll() { diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/FeedItemState.kt b/mage/src/main/java/mil/nga/giat/mage/feed/FeedItemState.kt index 956a0a9c5..7cf532542 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/FeedItemState.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/FeedItemState.kt @@ -2,7 +2,7 @@ package mil.nga.giat.mage.feed import android.content.Context import com.google.gson.JsonElement -import mil.nga.giat.mage.data.feed.ItemWithFeed +import mil.nga.giat.mage.database.model.feed.ItemWithFeed import mil.nga.giat.mage.map.FeedItemId import mil.nga.giat.mage.network.Server import mil.nga.giat.mage.network.gson.asLongOrNull diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/FeedScreen.kt b/mage/src/main/java/mil/nga/giat/mage/feed/FeedScreen.kt index b66074919..70533b947 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/FeedScreen.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/FeedScreen.kt @@ -23,11 +23,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.paging.CombinedLoadStates -import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems -import androidx.paging.compose.items +import androidx.paging.compose.itemKey import com.google.accompanist.glide.rememberGlidePainter import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.SwipeRefreshIndicator @@ -74,7 +72,7 @@ fun FeedScreen( onClose = { onClose?.invoke() } ) }, - content = { + content = { paddingValues -> SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = { onRefresh?.invoke() }, @@ -84,7 +82,8 @@ fun FeedScreen( refreshTriggerDistance = trigger, contentColor = MaterialTheme.colors.primary, ) - } + }, + modifier = Modifier.padding(paddingValues) ) { feedItems?.collectAsLazyPagingItems()?.let { items -> if (items.itemCount == 0) { @@ -133,8 +132,11 @@ private fun FeedContent( .padding(horizontal = 8.dp), contentPadding = PaddingValues(top = 16.dp) ) { - items(feedItems) { item -> - if (item != null) { + items( + count = feedItems.itemCount, + key = feedItems.itemKey { "${it.id.feedId}-${it.id.itemId}" } + ) { index -> + feedItems[index]?.let { item -> FeedItemContent( itemState = item, onLocationClick = { onItemAction?.invoke(FeedItemAction.Location(it)) }, diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/FeedViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/feed/FeedViewModel.kt index 407c17d85..1f118e1d8 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/FeedViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/FeedViewModel.kt @@ -11,7 +11,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.feed.* +import mil.nga.giat.mage.data.repository.feed.FeedRepository +import mil.nga.giat.mage.database.dao.feed.FeedDao +import mil.nga.giat.mage.database.dao.feed.FeedItemDao +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.ItemWithFeed import mil.nga.giat.mage.feed.item.SnackbarState import javax.inject.Inject @@ -27,11 +31,11 @@ class FeedViewModel @Inject constructor( get() = _snackbar.asStateFlow() private val _feedId = MutableLiveData() - val feed: LiveData = Transformations.switchMap(_feedId) { feedId -> + val feed: LiveData = _feedId.switchMap { feedId -> feedDao.feed(feedId) } - val feedItems: LiveData>> = Transformations.map(feed) { feed -> + val feedItems: LiveData>> = feed.map { feed -> Pager(PagingConfig(pageSize = 20)) { feedItemDao.pagingSource(feed.id) }.flow.cachedIn(viewModelScope).map { diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemScreen.kt b/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemScreen.kt index c215d0fe6..a68a00d4a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemScreen.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemScreen.kt @@ -78,8 +78,8 @@ fun FeedItemScreen( topBar = { FeedItemTopBar() { onClose?.invoke() } }, - content = { - Column { + content = { paddingValues -> + Column(Modifier.padding(paddingValues)) { FeedItemContent( itemState = itemState, onAction = onAction diff --git a/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemViewModel.kt index 99958402c..00411c799 100644 --- a/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/feed/item/FeedItemViewModel.kt @@ -5,13 +5,12 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import androidx.lifecycle.LiveData -import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.feed.FeedItemDao +import mil.nga.giat.mage.database.dao.feed.FeedItemDao import mil.nga.giat.mage.feed.FeedItemState import javax.inject.Inject diff --git a/mage/src/main/java/mil/nga/giat/mage/filter/FavoriteFilter.kt b/mage/src/main/java/mil/nga/giat/mage/filter/FavoriteFilter.kt deleted file mode 100644 index 82673b789..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/filter/FavoriteFilter.kt +++ /dev/null @@ -1,54 +0,0 @@ -package mil.nga.giat.mage.filter - -import android.content.Context -import android.util.Log -import com.j256.ormlite.stmt.QueryBuilder -import com.j256.ormlite.stmt.Where -import mil.nga.giat.mage.filter.FavoriteFilter -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationFavorite -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper -import mil.nga.giat.mage.sdk.exceptions.UserException -import java.sql.SQLException - -class FavoriteFilter(private val context: Context) : Filter { - - private var currentUser: User? = null - - init { - try { - currentUser = UserHelper.getInstance(context).readCurrentUser() - } catch (e: UserException) { - Log.e(LOG_NAME, "Error reading current user", e) - } - } - - @Throws(SQLException::class) - override fun query(): QueryBuilder? { - val user = currentUser ?: return null - - val observationFavoriteDao = DaoStore.getInstance(context).observationFavoriteDao - - val favoriteQb = observationFavoriteDao.queryBuilder() - favoriteQb.where() - .eq("user_id", user.remoteId) - .and() - .eq("is_favorite", true) - - return favoriteQb - } - - @Throws(SQLException::class) - override fun and(where: Where<*, Long>) {} - - override fun passesFilter(observation: Observation): Boolean { - return observation.favoritesMap[currentUser?.remoteId]?.isFavorite == true - } - - companion object { - private val LOG_NAME = FavoriteFilter::class.java.name - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/filter/ImportantFilter.kt b/mage/src/main/java/mil/nga/giat/mage/filter/ImportantFilter.kt deleted file mode 100644 index 557b11564..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/filter/ImportantFilter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package mil.nga.giat.mage.filter - -import android.content.Context -import com.j256.ormlite.stmt.QueryBuilder -import com.j256.ormlite.stmt.Where -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationImportant -import java.sql.SQLException - -class ImportantFilter(private val context: Context) : Filter { - - @Throws(SQLException::class) - override fun query(): QueryBuilder? { - val observationImportantDao = DaoStore.getInstance(context).observationImportantDao - val importantQb = observationImportantDao.queryBuilder() - importantQb.where().eq("is_important", true) - return importantQb - } - - @Throws(SQLException::class) - override fun and(where: Where<*, Long>) {} - - override fun passesFilter(observation: Observation): Boolean { - return observation.important?.isImportant == true - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/form/FormJson.kt b/mage/src/main/java/mil/nga/giat/mage/form/FormJson.kt index c627c3862..20221a7ed 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/FormJson.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/FormJson.kt @@ -11,12 +11,13 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import mil.nga.giat.mage.network.geometry.GeometryTypeAdapter import mil.nga.giat.mage.observation.ObservationLocation -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.gson.serializer.GeometrySerializer -import mil.nga.giat.mage.sdk.jackson.deserializer.GeometryDeserializer +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.network.geometry.GeometrySerializer import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory import mil.nga.sf.Geometry +import java.io.StringReader import java.lang.reflect.Type import java.text.ParseException import java.util.* @@ -76,7 +77,6 @@ open class FormField( open var value: T? = null set(value) { field = value -// notifyPropertyChanged(BR.value) } override fun equals(other: Any?): Boolean { @@ -346,7 +346,7 @@ class LocationParser : JsonDeserializer, JsonSerializer, JsonSerializer location = ObservationLocation(ObservationLocation.MANUAL_PROVIDER, geometry) } diff --git a/mage/src/main/java/mil/nga/giat/mage/form/FormViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/form/FormViewModel.kt index 4ec140849..408147770 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/FormViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/FormViewModel.kt @@ -1,6 +1,6 @@ package mil.nga.giat.mage.form -import android.content.Context +import android.app.Application import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -8,17 +8,25 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.gms.maps.model.LatLng import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.observation.State +import mil.nga.giat.mage.database.model.permission.Permission +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.form.Form.Companion.fromJson import mil.nga.giat.mage.form.defaults.FormPreferences import mil.nga.giat.mage.form.field.* import mil.nga.giat.mage.observation.* import mil.nga.giat.mage.observation.edit.MediaAction import mil.nga.giat.mage.observation.sync.ObservationSyncWorker -import mil.nga.giat.mage.sdk.datastore.observation.* -import mil.nga.giat.mage.sdk.datastore.user.* import mil.nga.giat.mage.sdk.event.IObservationEventListener import mil.nga.giat.mage.sdk.exceptions.ObservationException import mil.nga.giat.mage.sdk.exceptions.UserException @@ -27,7 +35,10 @@ import javax.inject.Inject @HiltViewModel open class FormViewModel @Inject constructor( - @ApplicationContext val context: Context + private val application: Application, + private val userLocalDataSource: UserLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource, + eventLocalDataSource: EventLocalDataSource ) : ViewModel() { companion object { @@ -41,7 +52,7 @@ open class FormViewModel @Inject constructor( protected val _observationState: MutableLiveData = MutableLiveData() val observationState: LiveData = _observationState - protected val event: Event = EventHelper.getInstance(context).currentEvent + val event = eventLocalDataSource.currentEvent val listener = object : IObservationEventListener { override fun onObservationUpdated(updated: Observation) { @@ -61,12 +72,12 @@ open class FormViewModel @Inject constructor( } init { - ObservationHelper.getInstance(context).addListener(listener) + observationLocalDataSource.addListener(listener) } override fun onCleared() { super.onCleared() - ObservationHelper.getInstance(context).removeListener(listener) + observationLocalDataSource.removeListener(listener) } open fun createObservation(timestamp: Date, location: ObservationLocation, defaultMapZoom: Float? = null, defaultMapCenter: LatLng? = null): Boolean { @@ -74,14 +85,14 @@ open class FormViewModel @Inject constructor( val forms = mutableListOf() val formDefinitions = mutableListOf() - event.forms.mapNotNull { form -> + event?.forms?.mapNotNull { form -> fromJson(form.json) } - .filterNot { it.archived } - .forEachIndexed { index, form -> + ?.filterNot { it.archived } + ?.forEachIndexed { index, form -> formDefinitions.add(form) - val defaultForm = FormPreferences(context, event.id, form.id).getDefaults() + val defaultForm = FormPreferences(application, event.id, form.id).getDefaults() val formMin = form.min ?: 0 val formCount = formMin + if (form.default && formMin == 0) 1 else 0 repeat(formCount) { @@ -98,11 +109,11 @@ open class FormViewModel @Inject constructor( var user: User? = null try { - user = UserHelper.getInstance(context).readCurrentUser() + user = userLocalDataSource.readCurrentUser() if (user != null) { observation.userId = user.remoteId } - } catch (ue: UserException) { } + } catch (_: UserException) { } val timestampFieldState = DateFieldState( DateFormField( @@ -131,8 +142,8 @@ open class FormViewModel @Inject constructor( geometryFieldState.answer = FieldValue.Location(ObservationLocation(location.geometry, location.provider, location.accuracy)) val definition = ObservationDefinition( - event.minObservationForms, - event.maxObservationForms, + event?.minObservationForms, + event?.maxObservationForms, forms = formDefinitions ) val observationState = ObservationState( @@ -153,7 +164,7 @@ open class FormViewModel @Inject constructor( this.observeChanges = observeChanges try { - val observation = ObservationHelper.getInstance(context).read(observationId) + val observation = observationLocalDataSource.read(observationId) createObservationState(observation, defaultMapZoom, defaultMapCenter) } catch (e: ObservationException) { Log.e(LOG_NAME, "Problem reading observation.", e) @@ -166,8 +177,9 @@ open class FormViewModel @Inject constructor( protected open fun createObservationState(observation: Observation, defaultMapZoom: Float? = null, defaultMapCenter: LatLng? = null) { _observation.value = observation + val currentEvent = event ?: return - val formDefinitions = event.forms + val formDefinitions = currentEvent.forms .mapNotNull { fromJson(it.json) } .associateBy { it.id } @@ -190,7 +202,7 @@ open class FormViewModel @Inject constructor( fields.add(fieldState) } - val formState = FormState(observationForm.id, observationForm.remoteId, event.remoteId, formDefinition, fields) + val formState = FormState(observationForm.id, observationForm.remoteId, currentEvent.remoteId, formDefinition, fields) formState.expanded.value = index == 0 forms.add(formState) } @@ -223,7 +235,7 @@ open class FormViewModel @Inject constructor( geometryFieldState.answer = FieldValue.Location(ObservationLocation(observation)) val currentUser: User? = try { - UserHelper.getInstance(context).readCurrentUser() + userLocalDataSource.readCurrentUser() } catch (ue: UserException) { null } val permissions = mutableSetOf() @@ -256,7 +268,7 @@ open class FormViewModel @Inject constructor( val importantState = if (observation.important?.isImportant == true) { val importantUser: User? = try { - UserHelper.getInstance(context).read(observation.important?.userId) + observation.important?.userId?.let { userLocalDataSource.read(it) } } catch (ue: UserException) { null } ObservationImportantState( @@ -266,7 +278,7 @@ open class FormViewModel @Inject constructor( } else null val user: User? = try { - UserHelper.getInstance(context).read(observation.userId) + userLocalDataSource.read(observation.userId) } catch (ue: UserException) { null } val observationState = ObservationState( @@ -315,11 +327,17 @@ open class FormViewModel @Inject constructor( for (fieldState in formState.fields) { val answer = fieldState.answer if (answer != null) { - properties.add(ObservationProperty(fieldState.definition.name, answer.serialize())) + properties.add( + ObservationProperty( + fieldState.definition.name, + answer.serialize() + ) + ) } } - val observationForm = ObservationForm() + val observationForm = + ObservationForm() observationForm.remoteId = formState.remoteId observationForm.formId = formState.definition.id observationForm.addProperties(properties) @@ -330,10 +348,10 @@ open class FormViewModel @Inject constructor( try { if (observation.id == null) { - val newObs = ObservationHelper.getInstance(context).create(observation) - Log.i(LOG_NAME, "Created new observation with id: " + newObs.id) + val newObs = observationLocalDataSource.create(observation) + Log.i(LOG_NAME, "Created new observation with id: " + newObs?.id) } else { - ObservationHelper.getInstance(context).update(observation) + observationLocalDataSource.update(observation) Log.i(LOG_NAME, "Updated observation with remote id: " + observation.remoteId) } } catch (e: java.lang.Exception) { @@ -372,11 +390,17 @@ open class FormViewModel @Inject constructor( for (fieldState in formState.fields) { val answer = fieldState.answer if (answer != null) { - properties.add(ObservationProperty(fieldState.definition.name, answer.serialize())) + properties.add( + ObservationProperty( + fieldState.definition.name, + answer.serialize() + ) + ) } } - val observationForm = ObservationForm() + val observationForm = + ObservationForm() observationForm.remoteId = formState.remoteId observationForm.formId = formState.definition.id observationForm.addProperties(properties) @@ -389,18 +413,20 @@ open class FormViewModel @Inject constructor( } fun deleteObservation() { - val observation = _observation.value - ObservationHelper.getInstance(context).archive(observation) + _observation.value?.let { + observationLocalDataSource.archive(it) + } } fun syncObservation() { - ObservationSyncWorker.scheduleWork(context) + ObservationSyncWorker.scheduleWork(application) } fun addForm(form: Form) { + val currentEvent = event ?: return val forms = observationState.value?.forms?.value?.toMutableList() ?: mutableListOf() - val defaultForm = FormPreferences(context, event.id, form.id).getDefaults() - val formState = FormState.fromForm(eventId = event.remoteId, form = form, defaultForm = defaultForm) + val defaultForm = FormPreferences(application, currentEvent.id, form.id).getDefaults() + val formState = FormState.fromForm(eventId = currentEvent.remoteId, form = form, defaultForm = defaultForm) formState.expanded.value = true forms.add(formState) _observationState.value?.forms?.value = forms @@ -411,7 +437,7 @@ open class FormViewModel @Inject constructor( val forms = observationState.value?.forms?.value?.toMutableList() ?: mutableListOf() forms.removeAt(index) observationState.value?.forms?.value = forms - } catch(e: IndexOutOfBoundsException) {} + } catch(_: IndexOutOfBoundsException) {} } fun reorderForms(forms: List) { @@ -452,25 +478,25 @@ open class FormViewModel @Inject constructor( } fun flagObservation(description: String?) { - val observation = _observation.value - val observationImportant = observation?.important + val observation = _observation.value ?: return + val observationImportant = observation.important val important = if (observationImportant == null) { val important = ObservationImportant() - observation?.important = important + observation.important = important important } else observationImportant try { val user: User? = try { - UserHelper.getInstance(context).readCurrentUser() + userLocalDataSource.readCurrentUser() } catch (e: Exception) { null } important.userId = user?.remoteId important.timestamp = Date() important.description = description - ObservationHelper.getInstance(context).addImportant(observation) + observationLocalDataSource.addImportant(observation) _observationState.value?.important?.value = ObservationImportantState(description = description, user = user?.displayName) } catch (e: ObservationException) { Log.e(LOG_NAME, "Error updating important flag for observation:" + observation?.remoteId) @@ -478,30 +504,26 @@ open class FormViewModel @Inject constructor( } fun unflagObservation() { - val observation = _observation.value + val observation = _observation.value ?: return try { - ObservationHelper.getInstance(context).removeImportant(observation) + observationLocalDataSource.removeImportant(observation) _observationState.value?.important?.value = null } catch (e: ObservationException) { - Log.e(LOG_NAME, "Error removing important flag for observation: " + observation?.remoteId) + Log.e(LOG_NAME, "Error removing important flag for observation: " + observation.remoteId) } } fun toggleFavorite() { - val observation = _observation.value + val observation = _observation.value ?: return + val user = userLocalDataSource.readCurrentUser() ?: return - val observationHelper = ObservationHelper.getInstance(context) val isFavorite: Boolean = _observationState.value?.favorite?.value == true try { - val user: User? = try { - UserHelper.getInstance(context).readCurrentUser() - } catch (e: Exception) { null } - if (isFavorite) { - observationHelper.unfavoriteObservation(observation, user) + observationLocalDataSource.unfavoriteObservation(observation, user) } else { - observationHelper.favoriteObservation(observation, user) + observationLocalDataSource.favoriteObservation(observation, user) } _observationState.value?.favorite?.value = !isFavorite @@ -511,8 +533,9 @@ open class FormViewModel @Inject constructor( } protected fun hasUpdatePermissionsInEventAcl(user: User?): Boolean { + val currentEvent = event ?: return false return if (user != null) { - event.acl[user.remoteId] + currentEvent.acl[user.remoteId] ?.asJsonObject ?.get("permissions") ?.asJsonArray diff --git a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultActivity.kt b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultActivity.kt index c3010a930..55d5440e5 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultActivity.kt @@ -16,9 +16,8 @@ import mil.nga.giat.mage.form.edit.dialog.GeometryFieldDialog import mil.nga.giat.mage.form.edit.dialog.SelectFieldDialog import mil.nga.giat.mage.form.field.* import mil.nga.giat.mage.observation.ObservationLocation -import mil.nga.giat.mage.sdk.datastore.user.Event +import mil.nga.giat.mage.database.model.event.Event import java.util.* -import javax.inject.Inject @AndroidEntryPoint class FormDefaultActivity : AppCompatActivity() { @@ -52,6 +51,7 @@ class FormDefaultActivity : AppCompatActivity() { setContent { FormDefaultScreen( + event = viewModel.event, formStateLiveData = viewModel.formState, onClose = { finish() }, onSave = { saveDefaults() }, diff --git a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultScreen.kt b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultScreen.kt index 681d83cda..ba4b5b593 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultScreen.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultScreen.kt @@ -12,11 +12,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.LiveData +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FieldType import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.edit.FieldEditContent @@ -25,6 +25,7 @@ import mil.nga.giat.mage.ui.theme.* @Composable fun FormDefaultScreen( + event: Event?, formStateLiveData: LiveData, onClose: (() -> Unit)? = null, onSave: (() -> Unit)? = null, @@ -32,7 +33,6 @@ fun FormDefaultScreen( onFieldClick: ((FieldState<*, *>) -> Unit)? = null ) { val formState by formStateLiveData.observeAsState() - val scope = rememberCoroutineScope() val scaffoldState = rememberScaffoldState() MageTheme { @@ -49,6 +49,7 @@ fun FormDefaultScreen( }, content = { Content( + event = event, formState = formState, onReset = { onReset?.invoke() @@ -92,11 +93,12 @@ fun TopBar( @Composable fun Content( + event: Event?, formState: FormState?, onReset: (() -> Unit)? = null, onFieldClick: ((FieldState<*, *>) -> Unit)? = null ) { - Surface() { + Surface { Column( Modifier .fillMaxHeight() @@ -112,6 +114,7 @@ fun Content( ) DefaultContent( + event = event, formState = formState, onReset = onReset, onFieldClick = onFieldClick @@ -164,6 +167,7 @@ fun DefaultHeader( @Composable fun DefaultContent( + event: Event?, formState: FormState, onReset: (() -> Unit)? = null, onFieldClick: ((FieldState<*, *>) -> Unit)? = null @@ -185,6 +189,7 @@ fun DefaultContent( } DefaultFormContent( + event = event, formState = formState, onFieldClick = onFieldClick ) @@ -210,6 +215,7 @@ fun DefaultContent( @Composable fun DefaultFormContent( + event: Event?, formState: FormState, onFieldClick: ((FieldState<*, *>) -> Unit)? = null ) { @@ -220,6 +226,7 @@ fun DefaultFormContent( for (fieldState in fields) { FieldEditContent( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + event = event, fieldState = fieldState, onClick = { onFieldClick?.invoke(fieldState) } ) diff --git a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultViewModel.kt index 89380e9b4..070eaba70 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/defaults/FormDefaultViewModel.kt @@ -9,15 +9,18 @@ import mil.nga.giat.mage.form.Form import mil.nga.giat.mage.form.FormField import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.* -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource import mil.nga.giat.mage.sdk.exceptions.EventException import javax.inject.Inject @HiltViewModel class FormDefaultViewModel @Inject constructor( val application: Application, + private val eventLocalDataSource: EventLocalDataSource ) : ViewModel() { + val event = eventLocalDataSource.currentEvent + private val _formState = MutableLiveData() val formState: LiveData = _formState @@ -28,16 +31,15 @@ class FormDefaultViewModel @Inject constructor( formPreferences = FormPreferences(application, eventId, formId) // TODO get this in background coroutine - val eventHelper: EventHelper = EventHelper.getInstance(application) try { - val event = eventHelper.read(eventId) - formJson = eventHelper.getForm(formId)?.json + val event = eventLocalDataSource.read(eventId) + formJson = eventLocalDataSource.getForm(formId)?.json Form.fromJson(formJson)?.let { form -> val defaultForm = FormPreferences(application, event.id, form.id).getDefaults() _formState.value = FormState.fromForm(eventId = event.remoteId, form = form, defaultForm = defaultForm) } - } catch (e: EventException) { } + } catch (_: EventException) { } } fun saveDefaults() { diff --git a/mage/src/main/java/mil/nga/giat/mage/form/edit/FormEdit.kt b/mage/src/main/java/mil/nga/giat/mage/form/edit/FormEdit.kt index 9b109c377..23a3cdfd5 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/edit/FormEdit.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/edit/FormEdit.kt @@ -28,18 +28,20 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.sp +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.field.* import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.view.AttachmentsViewContent import mil.nga.giat.mage.form.view.FormHeaderContent import mil.nga.giat.mage.observation.edit.AttachmentAction import mil.nga.giat.mage.observation.edit.MediaActionType -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import mil.nga.giat.mage.utils.DateFormatFactory import java.util.* @Composable fun FormEditContent( + event: Event?, formState: FormState, onFormDelete: (() -> Unit)? = null, onFieldClick: ((FieldState<*, *>) -> Unit)? = null, @@ -57,6 +59,7 @@ fun FormEditContent( for (fieldState in formState.fields.sortedBy { it.definition.id }) { FieldEditContent( modifier = Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp), + event = event, fieldState = fieldState, onClick = { onFieldClick?.invoke(fieldState) }, onMediaAction = { action -> onMediaAction?.invoke(action, fieldState.definition) }, @@ -85,11 +88,12 @@ fun FormEditContent( @Composable fun FieldEditContent( - modifier: Modifier = Modifier, - fieldState: FieldState<*, out FieldValue>, - onMediaAction: ((MediaActionType) -> Unit)? = null, - onAttachmentAction: ((AttachmentAction, Attachment) -> Unit)? = null, - onClick: (() -> Unit)? = null + modifier: Modifier = Modifier, + event: Event?, + fieldState: FieldState<*, out FieldValue>, + onMediaAction: ((MediaActionType) -> Unit)? = null, + onAttachmentAction: ((AttachmentAction, Attachment) -> Unit)? = null, + onClick: (() -> Unit)? = null ) { when (fieldState.definition.type) { FieldType.ATTACHMENT -> { @@ -136,7 +140,8 @@ fun FieldEditContent( } FieldType.GEOMETRY -> { GeometryEdit( - modifier, + modifier = modifier, + event = event, fieldState as GeometryFieldState, onClick = onClick ) @@ -210,10 +215,10 @@ fun FieldEditContent( @Composable fun AttachmentEdit( - modifier: Modifier = Modifier, - fieldState: AttachmentFieldState, - onAttachmentAction: ((AttachmentAction, Attachment) -> Unit)? = null, - onMediaAction: ((MediaActionType) -> Unit)? = null + modifier: Modifier = Modifier, + fieldState: AttachmentFieldState, + onAttachmentAction: ((AttachmentAction, Attachment) -> Unit)? = null, + onMediaAction: ((MediaActionType) -> Unit)? = null ) { val attachments = fieldState.answer?.attachments?.filter { it.action != Media.ATTACHMENT_DELETE_ACTION } ?: listOf() var size by remember { mutableStateOf(attachments.size) } diff --git a/mage/src/main/java/mil/nga/giat/mage/form/edit/GeometryEditContent.kt b/mage/src/main/java/mil/nga/giat/mage/form/edit/GeometryEditContent.kt index 0a614d8a2..8b48e32ae 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/edit/GeometryEditContent.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/edit/GeometryEditContent.kt @@ -8,11 +8,11 @@ import androidx.compose.material.icons.outlined.Place import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import mil.nga.giat.mage.coordinate.CoordinateFormatter +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.GeometryFieldState import mil.nga.giat.mage.form.view.MapState @@ -23,6 +23,7 @@ import mil.nga.giat.mage.ui.theme.warning @Composable fun GeometryEdit( modifier: Modifier = Modifier, + event: Event?, fieldState: GeometryFieldState, formState: FormState? = null, onClick: (() -> Unit)? = null @@ -71,7 +72,13 @@ fun GeometryEdit( .height(150.dp) ) { val mapView = rememberMapViewWithLifecycle() - MapViewContent(mapView, MapState(fieldState.defaultMapCenter, fieldState.defaultMapZoom), formState, location) + MapViewContent( + map = mapView, + mapState = MapState(fieldState.defaultMapCenter, fieldState.defaultMapZoom), + event = event, + formState = formState, + location = location + ) } } diff --git a/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/FormReorderDialog.kt b/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/FormReorderDialog.kt index a456bb308..07b3d689f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/FormReorderDialog.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/FormReorderDialog.kt @@ -10,7 +10,6 @@ import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.fragment.app.DialogFragment -import androidx.lifecycle.ViewModelProviders import mil.nga.giat.mage.R import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.FormViewModel @@ -18,6 +17,7 @@ import mil.nga.giat.mage.observation.ObservationState import android.view.MotionEvent import android.widget.ImageView import androidx.core.widget.ImageViewCompat +import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.* import mil.nga.giat.mage.databinding.DialogFormReorderBinding import mil.nga.giat.mage.databinding.ViewFormReorderItemBinding @@ -79,7 +79,8 @@ class FormReorderDialog : DialogFragment() { setStyle(STYLE_NORMAL, R.style.AppTheme_Dialog_Fullscreen) viewModel = activity?.run { - ViewModelProviders.of(this).get(FormViewModel::class.java) + ViewModelProvider(this)[FormViewModel::class.java] + } ?: throw Exception("Invalid Activity") } diff --git a/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/GeometryFieldDialog.kt b/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/GeometryFieldDialog.kt index 1bbad5d16..ba646fbb9 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/GeometryFieldDialog.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/edit/dialog/GeometryFieldDialog.kt @@ -1003,6 +1003,7 @@ class GeometryFieldDialog : DialogFragment(), getString(R.string.location_edit_hint_shape) } } + else -> {} } binding.hintText.text = hint diff --git a/mage/src/main/java/mil/nga/giat/mage/form/field/AttachmentFieldState.kt b/mage/src/main/java/mil/nga/giat/mage/form/field/AttachmentFieldState.kt index 6b40875bb..a88482579 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/field/AttachmentFieldState.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/field/AttachmentFieldState.kt @@ -2,8 +2,7 @@ package mil.nga.giat.mage.form.field import mil.nga.giat.mage.form.AttachmentFormField import mil.nga.giat.mage.form.FormField -import mil.nga.giat.mage.form.NumberFormField -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment class AttachmentFieldState(definition: FormField) : FieldState( diff --git a/mage/src/main/java/mil/nga/giat/mage/form/field/FieldState.kt b/mage/src/main/java/mil/nga/giat/mage/form/field/FieldState.kt index 7a3c7b34b..dcb511149 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/field/FieldState.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/field/FieldState.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import mil.nga.giat.mage.form.* import mil.nga.giat.mage.observation.ObservationLocation -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.utils.GeometryUtility +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.sdk.utils.toGeometry open class FieldState ( val definition: FormField, @@ -103,13 +103,13 @@ open class FieldState ( val location = if (default != null) { if (default is ByteArray) { - ObservationLocation(GeometryUtility.toGeometry(default)) + ObservationLocation(default.toGeometry()) } else { default as? ObservationLocation } } else { if (value is ByteArray) { - ObservationLocation(GeometryUtility.toGeometry(value)) + ObservationLocation(value.toGeometry()) } else { value as? ObservationLocation } diff --git a/mage/src/main/java/mil/nga/giat/mage/form/field/FieldValue.kt b/mage/src/main/java/mil/nga/giat/mage/form/field/FieldValue.kt index 7eeb5cd24..eab5d5ef1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/field/FieldValue.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/field/FieldValue.kt @@ -2,8 +2,8 @@ package mil.nga.giat.mage.form.field import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import mil.nga.giat.mage.observation.ObservationLocation -import mil.nga.giat.mage.sdk.utils.GeometryUtility import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import mil.nga.giat.mage.sdk.utils.toBytes import java.io.Serializable class Media { @@ -14,7 +14,7 @@ class Media { } sealed class FieldValue { - class Attachment(attachments: List) : FieldValue() { + class Attachment(attachments: List) : FieldValue() { val attachments by mutableStateOf(attachments) } @@ -45,7 +45,7 @@ sealed class FieldValue { is Attachment -> attachments as Serializable is Boolean -> boolean is Date -> date - is Location -> GeometryUtility.toGeometryBytes(location.geometry) + is Location -> location.geometry.toBytes() is Multi -> choices as Serializable is Number -> { val value = number.toDouble() diff --git a/mage/src/main/java/mil/nga/giat/mage/form/view/AttachmentViewContent.kt b/mage/src/main/java/mil/nga/giat/mage/form/view/AttachmentViewContent.kt index 27ff0a794..0dd37815e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/view/AttachmentViewContent.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/view/AttachmentViewContent.kt @@ -18,7 +18,7 @@ import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.google.accompanist.glide.rememberGlidePainter import mil.nga.giat.mage.glide.transform.VideoOverlayTransformation import mil.nga.giat.mage.observation.edit.AttachmentAction -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.util.* @Composable diff --git a/mage/src/main/java/mil/nga/giat/mage/form/view/FormView.kt b/mage/src/main/java/mil/nga/giat/mage/form/view/FormView.kt index 938ce5984..b7af5142e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/view/FormView.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/view/FormView.kt @@ -27,15 +27,15 @@ import mil.nga.giat.mage.coordinate.CoordinateFormatter import mil.nga.giat.mage.form.FieldType import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.* -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import mil.nga.giat.mage.utils.DateFormatFactory import java.util.* @Composable fun FormContent( - formState: FormState, - onAttachmentClick: ((Attachment) -> Unit)? = null, - onLocationClick: ((String) -> Unit)? = null + formState: FormState, + onAttachmentClick: ((Attachment) -> Unit)? = null, + onLocationClick: ((String) -> Unit)? = null ) { Card( Modifier @@ -155,10 +155,10 @@ fun FormHeaderContent( @Composable fun FieldContent( - modifier: Modifier = Modifier, - fieldState: FieldState<*, out FieldValue>, - onAttachmentClick: ((Attachment) -> Unit)? = null, - onLocationClick: ((String) -> Unit)? = null + modifier: Modifier = Modifier, + fieldState: FieldState<*, out FieldValue>, + onAttachmentClick: ((Attachment) -> Unit)? = null, + onLocationClick: ((String) -> Unit)? = null ) { when (fieldState) { is BooleanFieldState -> { diff --git a/mage/src/main/java/mil/nga/giat/mage/form/view/MapViewContent.kt b/mage/src/main/java/mil/nga/giat/mage/form/view/MapViewContent.kt index 93cd3ca5b..381e2e966 100644 --- a/mage/src/main/java/mil/nga/giat/mage/form/view/MapViewContent.kt +++ b/mage/src/main/java/mil/nga/giat/mage/form/view/MapViewContent.kt @@ -19,6 +19,7 @@ import com.google.maps.android.ktx.* import kotlinx.coroutines.launch import mil.nga.geopackage.map.geom.GoogleMapShapeConverter import mil.nga.giat.mage.R +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.FieldValue import mil.nga.giat.mage.glide.target.MarkerTarget @@ -34,6 +35,7 @@ data class MapState(val center: LatLng?, val zoom: Float?) fun MapViewContent( map: MapView, mapState: MapState, + event: Event?, formState: FormState?, location: ObservationLocation ) { @@ -102,7 +104,7 @@ fun MapViewContent( } } else { val shape = GoogleMapShapeConverter().toShape(location.geometry).shape - val style = ShapeStyle.fromForm(formState, context) + val style = ShapeStyle.fromForm(event, formState, context) if (shape is PolylineOptions) { googleMap.addPolyline { diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/MageGlideModule.kt b/mage/src/main/java/mil/nga/giat/mage/glide/MageGlideModule.kt index 85c75c703..a85968b06 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/MageGlideModule.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/MageGlideModule.kt @@ -5,24 +5,40 @@ import android.graphics.Bitmap import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.Registry +import com.bumptech.glide.annotation.Excludes import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.integration.okhttp3.OkHttpLibraryGlideModule import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.AppGlideModule -import mil.nga.giat.mage.glide.loader.* +import dagger.hilt.EntryPoint +import dagger.hilt.EntryPoints +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.glide.loader.AudioLoader +import mil.nga.giat.mage.glide.loader.AvatarLoader +import mil.nga.giat.mage.glide.loader.DocumentLoader +import mil.nga.giat.mage.glide.loader.ImageFileLoader +import mil.nga.giat.mage.glide.loader.ImageUrlLoader +import mil.nga.giat.mage.glide.loader.MapIconLoader +import mil.nga.giat.mage.glide.loader.VideoFileLoader +import mil.nga.giat.mage.glide.loader.VideoUrlLoader import mil.nga.giat.mage.glide.model.Avatar import mil.nga.giat.mage.map.annotation.MapAnnotation -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.http.HttpClientManager +import okhttp3.OkHttpClient import java.io.InputStream import java.nio.ByteBuffer @GlideModule +@Excludes(OkHttpLibraryGlideModule::class) class MageGlideModule : AppGlideModule() { - companion object { - private const val DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024 + @EntryPoint + @InstallIn(SingletonComponent::class) + interface GlideEntryPoint { + fun provideOkHttpClient(): OkHttpClient } override fun applyOptions(context: Context, builder: GlideBuilder) { @@ -30,15 +46,21 @@ class MageGlideModule : AppGlideModule() { } override fun registerComponents(context: Context, glide: Glide, registry: Registry) { - registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(HttpClientManager.getInstance().httpClient())) + val entryPoint = EntryPoints.get(context, GlideEntryPoint::class.java) + val httpClient = entryPoint.provideOkHttpClient() + registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(httpClient)) registry.append(Attachment::class.java, InputStream::class.java, AudioLoader.Factory()) registry.append(Attachment::class.java, ByteBuffer::class.java, VideoUrlLoader.Factory(context)) registry.append(Attachment::class.java, Bitmap::class.java, VideoFileLoader.Factory()) - registry.append(Attachment::class.java, InputStream::class.java, ImageUrlLoader.Factory(context)) + registry.append(Attachment::class.java, InputStream::class.java, ImageUrlLoader.Factory()) registry.append(Attachment::class.java, InputStream::class.java, ImageFileLoader.Factory()) registry.append(Attachment::class.java, InputStream::class.java, DocumentLoader.Factory()) registry.append(Avatar::class.java, InputStream::class.java, AvatarLoader.Factory(context)) registry.append(MapAnnotation::class.java, InputStream::class.java, MapIconLoader.Factory()) } + + companion object { + private const val DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024 + } } diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/AudioLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/AudioLoader.kt index 77f16b6f3..c3644d0db 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/AudioLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/AudioLoader.kt @@ -6,7 +6,7 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.io.InputStream class AudioLoader private constructor(private val fileLoader: ModelLoader) : ModelLoader { diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/AvatarLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/AvatarLoader.kt index 93e675543..e83288488 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/AvatarLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/AvatarLoader.kt @@ -10,7 +10,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import mil.nga.giat.mage.R import mil.nga.giat.mage.glide.model.Avatar -import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.io.File import java.io.InputStream @@ -31,7 +31,7 @@ class AvatarLoader private constructor(private val context: Context, private val return if (model.localUri != null) { fileLoader.buildLoadData(File(model.localUri), width, height, options) } else if (model.remoteUri != null) { - val stringURL = getUrl(model, width, height, options) + val stringURL = getUrl(model) if (TextUtils.isEmpty(stringURL)) { return null } @@ -43,10 +43,10 @@ class AvatarLoader private constructor(private val context: Context, private val } } - fun getUrl(user: Avatar, width: Int, height: Int, options: Options): String? { + private fun getUrl(user: Avatar): String? { if (user.remoteUri == null) return null - var url = HttpUrl.parse(user.remoteUri) + var url = user.remoteUri.toHttpUrlOrNull() ?.newBuilder() ?.addQueryParameter("_dc", user.lastModified.toString()) ?.build() @@ -54,7 +54,7 @@ class AvatarLoader private constructor(private val context: Context, private val // TODO can remove this once server bug is fixed to return full avatar url if (url == null) { PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue))?.let { - url = HttpUrl.parse(it + user.remoteUri) + url = (it + user.remoteUri).toHttpUrlOrNull() ?.newBuilder() ?.addQueryParameter("_dc", user.lastModified.toString()) ?.build() diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/DocumentLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/DocumentLoader.kt index 41276eb3f..3abdaa698 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/DocumentLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/DocumentLoader.kt @@ -6,7 +6,7 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.io.InputStream import java.util.* diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageFileLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageFileLoader.kt index 2503d2457..076c76b7b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageFileLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageFileLoader.kt @@ -5,7 +5,7 @@ import com.bumptech.glide.load.Options import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.io.File import java.io.InputStream diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageUrlLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageUrlLoader.kt index b450499fe..c3032abd4 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageUrlLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/ImageUrlLoader.kt @@ -1,31 +1,31 @@ package mil.nga.giat.mage.glide.loader -import android.content.Context import com.bumptech.glide.load.Options import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.io.InputStream -class ImageUrlLoader private constructor(private val context: Context, concreteLoader: ModelLoader) : BaseGlideUrlLoader(concreteLoader, null) { +class ImageUrlLoader private constructor(concreteLoader: ModelLoader) : BaseGlideUrlLoader(concreteLoader, null) { - class Factory(private val context: Context) : ModelLoaderFactory { + class Factory : ModelLoaderFactory { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - return ImageUrlLoader(context, multiFactory.build(GlideUrl::class.java, InputStream::class.java)) + return ImageUrlLoader(multiFactory.build(GlideUrl::class.java, InputStream::class.java)) } override fun teardown() {} } - override fun getUrl(attachment: Attachment, width: Int, height: Int, options: Options): String? { - val url = HttpUrl.parse(attachment.url)!! + override fun getUrl(attachment: Attachment, width: Int, height: Int, options: Options): String { + val url = attachment.url.toHttpUrlOrNull()!! .newBuilder() - .addQueryParameter("size", Math.max(width, height).toString()) + .addQueryParameter("size", width.coerceAtLeast(height).toString()) .build() return url.toString() diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoFileLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoFileLoader.kt index fd55b867b..76ec63d0c 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoFileLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoFileLoader.kt @@ -14,8 +14,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.signature.ObjectKey -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import java.io.File +import mil.nga.giat.mage.database.model.observation.Attachment /** * Created by wnewman diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoUrlLoader.kt b/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoUrlLoader.kt index 875bbba7d..1d6d5930e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoUrlLoader.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/loader/VideoUrlLoader.kt @@ -13,7 +13,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.signature.ObjectKey import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.io.ByteArrayOutputStream import java.nio.ByteBuffer diff --git a/mage/src/main/java/mil/nga/giat/mage/glide/model/Avatar.kt b/mage/src/main/java/mil/nga/giat/mage/glide/model/Avatar.kt index afa5a0494..6284e2342 100644 --- a/mage/src/main/java/mil/nga/giat/mage/glide/model/Avatar.kt +++ b/mage/src/main/java/mil/nga/giat/mage/glide/model/Avatar.kt @@ -1,6 +1,6 @@ package mil.nga.giat.mage.glide.model -import mil.nga.giat.mage.sdk.datastore.user.User +import mil.nga.giat.mage.database.model.user.User data class Avatar( val remoteUri: String?, diff --git a/mage/src/main/java/mil/nga/giat/mage/location/LocationFetchService.kt b/mage/src/main/java/mil/nga/giat/mage/location/LocationFetchService.kt index aaae28f1d..fa681ac9b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/location/LocationFetchService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/location/LocationFetchService.kt @@ -12,7 +12,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.location.LocationRepository +import mil.nga.giat.mage.data.repository.location.LocationRepository import javax.inject.Inject @AndroidEntryPoint diff --git a/mage/src/main/java/mil/nga/giat/mage/location/LocationPolicy.kt b/mage/src/main/java/mil/nga/giat/mage/location/LocationPolicy.kt index d092e7767..e8ed2f6b4 100644 --- a/mage/src/main/java/mil/nga/giat/mage/location/LocationPolicy.kt +++ b/mage/src/main/java/mil/nga/giat/mage/location/LocationPolicy.kt @@ -6,7 +6,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class LocationPolicy @Inject constructor(val locationProvider: LocationProvider) { +class LocationPolicy @Inject constructor(locationProvider: LocationProvider) { var bestLocationProvider: MediatorLiveData = MediatorLiveData() diff --git a/mage/src/main/java/mil/nga/giat/mage/location/LocationProvider.kt b/mage/src/main/java/mil/nga/giat/mage/location/LocationProvider.kt index 52c6a01fd..6781eab60 100644 --- a/mage/src/main/java/mil/nga/giat/mage/location/LocationProvider.kt +++ b/mage/src/main/java/mil/nga/giat/mage/location/LocationProvider.kt @@ -54,7 +54,7 @@ constructor(@ApplicationContext val context: Context, val preferences: SharedPre preferences.unregisterOnSharedPreferenceChangeListener(this) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(preferences: SharedPreferences?, key: String?) { if (key.equals(context.getString(R.string.gpsSensitivityKey), ignoreCase = true)) { Log.d(LOG_NAME, "GPS sensitivity changed, distance in meters for change: $minimumDistanceChangeForUpdates") minimumDistanceChangeForUpdates = getMinimumDistanceChangeForUpdates() @@ -65,6 +65,7 @@ constructor(@ApplicationContext val context: Context, val preferences: SharedPre } } + private fun requestLocationUpdates() { Log.v(LOG_NAME, "request location updates") diff --git a/mage/src/main/java/mil/nga/giat/mage/location/LocationReportingService.kt b/mage/src/main/java/mil/nga/giat/mage/location/LocationReportingService.kt index d7ad5eb33..5cbba8b49 100644 --- a/mage/src/main/java/mil/nga/giat/mage/location/LocationReportingService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/location/LocationReportingService.kt @@ -19,7 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import mil.nga.giat.mage.MageApplication import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.location.LocationRepository +import mil.nga.giat.mage.data.repository.location.LocationRepository import mil.nga.giat.mage.login.LoginActivity import javax.inject.Inject @@ -96,7 +96,7 @@ open class LocationReportingService : LifecycleService(), Observer, Sh preferences.unregisterOnSharedPreferenceChangeListener(this) } - override fun onChanged(location: Location?) { + override fun onChanged(location: Location) { if (shouldReportLocation && location?.provider == LocationManager.GPS_PROVIDER) { Log.v(LOG_NAME, "GPS location changed") @@ -117,7 +117,7 @@ open class LocationReportingService : LifecycleService(), Observer, Sh } } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key.equals(getString(R.string.reportLocationKey), ignoreCase = true)) { shouldReportLocation = getShouldReportLocation() Log.d(LOG_NAME, "Report location changed $shouldReportLocation") diff --git a/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationButton.kt b/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationButton.kt index 715f19cf8..cf404cf76 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationButton.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationButton.kt @@ -60,7 +60,7 @@ class AuthenticationButton @JvmOverloads constructor( configureDefaultIcon(strategy.getString("buttonColor")) } } catch (e: JSONException) { - e.printStackTrace() + Log.e(LOG_NAME, "Error parsing authentication strategy json", e) } } diff --git a/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationStatus.kt b/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationStatus.kt new file mode 100644 index 000000000..381798b34 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/login/AuthenticationStatus.kt @@ -0,0 +1,8 @@ +package mil.nga.giat.mage.login + +sealed class AuthenticationStatus { + data class Success(val username: String, val token: String): AuthenticationStatus() + data class Offline(val message: String): AuthenticationStatus() + data class Failure(val code: Int, val message: String): AuthenticationStatus() + data class AccountCreated(val message: String): AuthenticationStatus() +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/login/AuthorizationStatus.kt b/mage/src/main/java/mil/nga/giat/mage/login/AuthorizationStatus.kt new file mode 100644 index 000000000..7417ade68 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/login/AuthorizationStatus.kt @@ -0,0 +1,14 @@ +package mil.nga.giat.mage.login + +import mil.nga.giat.mage.database.model.user.User + +sealed class AuthorizationStatus { + data class Success( + val user: User, + val token: String, + val tokenExpiration: String + ): AuthorizationStatus() + object FailInvalidServer : AuthorizationStatus() + data class FailAuthorization(val user: User? = null) : AuthorizationStatus() + data class FailAuthentication(val user: User? = null, val message: String) : AuthorizationStatus() +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.java b/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.java deleted file mode 100644 index 6c9620402..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.java +++ /dev/null @@ -1,525 +0,0 @@ -package mil.nga.giat.mage.login; - -import android.app.Activity; -import android.app.Dialog; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.content.SharedPreferences; -import android.graphics.Typeface; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentTransaction; -import androidx.lifecycle.ViewModelProvider; -import androidx.work.WorkManager; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; - -import org.apache.commons.lang3.StringUtils; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import mil.nga.giat.mage.LandingActivity; -import mil.nga.giat.mage.MageApplication; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.compat.server5.login.SignupActivityServer5; -import mil.nga.giat.mage.cache.CacheUtils; -import mil.nga.giat.mage.contact.ContactDialog; -import mil.nga.giat.mage.disclaimer.DisclaimerActivity; -import mil.nga.giat.mage.event.EventsActivity; -import mil.nga.giat.mage.login.LoginViewModel.Authentication; -import mil.nga.giat.mage.login.idp.IdpLoginFragment; -import mil.nga.giat.mage.login.ldap.LdapLoginFragment; -import mil.nga.giat.mage.login.mage.MageLoginFragment; -import mil.nga.giat.mage.sdk.Compatibility; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.exceptions.UserException; -import mil.nga.giat.mage.sdk.login.AuthenticationStatus; -import mil.nga.giat.mage.sdk.login.AuthorizationStatus; -import mil.nga.giat.mage.sdk.preferences.PreferenceHelper; -import mil.nga.giat.mage.sdk.utils.MediaUtility; -import mil.nga.giat.mage.sdk.utils.UserUtility; - -@AndroidEntryPoint -public class LoginActivity extends AppCompatActivity { - - public static final String EXTRA_CONTINUE_SESSION = "CONTINUE_SESSION"; - public static final String EXTRA_CONTINUE_SESSION_WHILE_USING = "CONTINUE_SESSION_WHILE_USING"; - - @Inject - protected MageApplication application; - - @Inject - protected SharedPreferences preferences; - - private LoginViewModel viewModel; - - private TextView mServerURL; - - private String mOpenFilePath; - - private boolean mContinueSession; - - public final TextView getServerUrlText() { - return mServerURL; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - - mContinueSession = getIntent().getBooleanExtra(EXTRA_CONTINUE_SESSION, false); - - boolean continueSessionWhileUsing = getIntent().getBooleanExtra(EXTRA_CONTINUE_SESSION_WHILE_USING, false); - intent.removeExtra(EXTRA_CONTINUE_SESSION_WHILE_USING); - if (continueSessionWhileUsing && savedInstanceState == null) { - showSessionExpiredDialog(); - } - - if (intent.getBooleanExtra("LOGOUT", false)) { - application.onLogout(true, null); - } - - // IMPORTANT: load the configuration from preferences files and server - PreferenceHelper preferenceHelper = PreferenceHelper.getInstance(getApplicationContext()); - preferenceHelper.initialize(false, R.xml.class); - - // check if the database needs to be upgraded, and if so log them out - if (DaoStore.DATABASE_VERSION != preferences.getInt(getResources().getString(R.string.databaseVersionKey), 0)) { - application.onLogout(true, null); - } - - preferences.edit().putInt(getString(R.string.databaseVersionKey), DaoStore.DATABASE_VERSION).apply(); - - // check google play services version - int isGooglePlayServicesAvailable = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getApplicationContext()); - if (isGooglePlayServicesAvailable != ConnectionResult.SUCCESS) { - if (GooglePlayServicesUtil.isUserRecoverableError(isGooglePlayServicesAvailable)) { - Dialog dialog = GooglePlayServicesUtil.getErrorDialog(isGooglePlayServicesAvailable, this, 1); - dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - dialog.dismiss(); - finish(); - } - }); - dialog.show(); - } else { - new AlertDialog.Builder(this).setTitle("Google Play Services").setMessage("Google Play Services is not installed, or needs to be updated. Please update Google Play Services before continuing.").setPositiveButton(android.R.string.ok, new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - finish(); - } - }).show(); - } - } - - // Handle when MAGE was launched with a Uri (such as a local or remote cache file) - Uri uri = intent.getData(); - if (uri == null) { - Bundle bundle = intent.getExtras(); - if (bundle != null) { - Object objectUri = bundle.get(Intent.EXTRA_STREAM); - if (objectUri != null) { - uri = (Uri) objectUri; - } - } - } - if (uri != null) { - handleUri(uri); - } - - // if token is not expired, then skip the login module - if (!UserUtility.getInstance(getApplicationContext()).isTokenExpired()) { - skipLogin(); - } else { - // temporarily prune complete work on every login to ensure our unique work is rescheduled - WorkManager.getInstance(getApplicationContext()).pruneWork(); - application.stopLocationService(); - } - - // no title bar - setContentView(R.layout.activity_login); - hideKeyboardOnClick(findViewById(R.id.login)); - - TextView appName = findViewById(R.id.mage); - appName.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/GondolaMage-Regular.otf")); - - ((TextView) findViewById(R.id.login_version)).setText("App Version: " + preferences.getString(getString(R.string.buildVersionKey), "NA")); - - mServerURL = findViewById(R.id.server_url); - - String serverUrl = preferences.getString(getString(R.string.serverURLKey), getString(R.string.serverURLDefaultValue)); - if (StringUtils.isEmpty(serverUrl)) { - changeServerURL(); - return; - } - - findViewById(R.id.server_url).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - changeServerURL(); - } - }); - - mServerURL.setText(serverUrl); - - // Setup login based on last api pull - configureLogin(); - - viewModel = new ViewModelProvider(this).get(LoginViewModel.class); - viewModel.getApiStatus().observe(this, valid -> observeApi()); - - viewModel.getAuthenticationState().observe(this, this::observeAuthenticationState); - viewModel.getAuthenticationStatus().observe(this, this::observeAuthentication); - viewModel.getAuthorizationStatus().observe(this, this::observeAuthorization); - - viewModel.api(serverUrl); - } - - @Override - public void onBackPressed() { - if (mContinueSession) { - // In this case the activity stack was preserved. Don't allow the user to go back to an activity without logging in. - // Since this is the application entry point, assume back means go home. - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_HOME); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - return; - } - - super.onBackPressed(); - } - - - private void observeApi() { - configureLogin(); - } - - private void observeAuthenticationState(LoginViewModel.AuthenticationState state) { - if (state == LoginViewModel.AuthenticationState.LOADING) { - findViewById(R.id.login_status).setVisibility(View.VISIBLE); - findViewById(R.id.login_form).setVisibility(View.GONE); - } else if (state == LoginViewModel.AuthenticationState.ERROR) { - findViewById(R.id.login_status).setVisibility(View.GONE); - findViewById(R.id.login_form).setVisibility(View.VISIBLE); - } - } - - private void observeAuthentication(Authentication authentication) { - if (authentication == null) return; - - AuthenticationStatus status = authentication.getStatus(); - - if (status.getStatus() == AuthenticationStatus.Status.SUCCESSFUL_AUTHENTICATION) { - viewModel.authorize(authentication.getStrategy(), status.getToken()); - } else if (status.getStatus() == AuthenticationStatus.Status.DISCONNECTED_AUTHENTICATION) { - new AlertDialog.Builder(this) - .setTitle("Disconnected Login") - .setMessage("You are logging into MAGE in disconnected mode. You must re-establish a connection in order to push and pull information to and from your server.") - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - loginComplete(false); - dialog.dismiss(); - }) - .setCancelable(false) - .show(); - } else if (status.getStatus() == AuthenticationStatus.Status.ACCOUNT_CREATED) { - String message = status.getMessage(); - if (message == null) { - message = "Please contact a MAGE administrator to activate your account."; - } - - ContactDialog dialog = new ContactDialog(this, this.preferences, "Account Created", message); - dialog.setAuthenticationStrategy(authentication.getStrategy()); - dialog.show(); - } else { - String message = status.getMessage(); - if (message == null) { - message = "Authentication error, please contact your MAGE administrator for assistance."; - } - - ContactDialog dialog = new ContactDialog(this, this.preferences, "Sign in Failed", message); - dialog.setAuthenticationStrategy(authentication.getStrategy()); - dialog.show(); - } - } - - private void observeAuthorization(LoginViewModel.Authorization authorization) { - if (authorization == null) return; - - AuthorizationStatus.Status status = authorization.getStatus().getStatus(); - if (status == AuthorizationStatus.Status.SUCCESSFUL_AUTHORIZATION) { - loginComplete(authorization.getUserChanged()); - } else if (status == AuthorizationStatus.Status.FAILED_AUTHORIZATION) { - ContactDialog dialog = new ContactDialog(this, this.preferences, "Registration Sent", getString(R.string.device_registered_text)); - User user = authorization.getStatus().getUser(); - if (user != null) { - dialog.setUsername(user.getUsername()); - } - dialog.show(); - } else if (status == AuthorizationStatus.Status.INVALID_SERVER) { - ContactDialog dialog = new ContactDialog(this, this.preferences, "Application Compatibility Error", "This app is not compatible with this server. Please update your context or talk to your MAGE administrator."); - dialog.show(); - } else { - String message = authorization.getStatus().getMessage(); - if (message == null) { - message = "Authorization error, please contact your MAGE administrator for assistance"; - } - - ContactDialog dialog = new ContactDialog(this, this.preferences, "Sign-in Failed", message); - - User user = authorization.getStatus().getUser(); - if (user != null) { - dialog.setUsername(user.getUsername()); - } - - dialog.show(); - } - } - - private void configureLogin() { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - - Map strategies = new TreeMap<>(); - - // TODO marshal authentication strategies to POJOs with Jackson - JSONObject authenticationStrategies = PreferenceHelper.getInstance(getApplicationContext()).getAuthenticationStrategies(); - Iterator iterator = authenticationStrategies.keys(); - while (iterator.hasNext()) { - String strategyKey = iterator.next(); - try { - JSONObject strategy = (JSONObject) authenticationStrategies.get(strategyKey); - if ("local".equals(strategyKey)) { - strategy.putOpt("type", strategyKey); - } - - strategies.put(strategyKey, strategy); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - if (strategies.size() > 1 && strategies.containsKey("local")) { - findViewById(R.id.or).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.or).setVisibility(View.GONE); - } - - findViewById(R.id.google_login_button).setVisibility(View.GONE); - for (final Map.Entry entry : strategies.entrySet()) { - String authenticationName = entry.getKey(); - String authenticationType = entry.getValue().optString("type"); - - if (getSupportFragmentManager().findFragmentByTag(authenticationName) != null) continue; - - if ("local".equals(authenticationName)) { - Fragment loginFragment = MageLoginFragment.Companion.newInstance(entry.getKey(), entry.getValue()); - transaction.add(R.id.local_auth, loginFragment, authenticationName); - } else if ("oauth".equals(authenticationType)) { - Fragment loginFragment = IdpLoginFragment.Companion.newInstance(entry.getKey(), entry.getValue()); - transaction.add(R.id.third_party_auth, loginFragment, authenticationName); - } else if ("saml".equals(authenticationType)) { - Fragment loginFragment = IdpLoginFragment.Companion.newInstance(entry.getKey(), entry.getValue()); - transaction.add(R.id.third_party_auth, loginFragment, authenticationName); - } else if ("ldap".equals(authenticationType)) { - Fragment loginFragment = LdapLoginFragment.Companion.newInstance(entry.getKey(), entry.getValue()); - transaction.add(R.id.third_party_auth, loginFragment, authenticationName); - } else { - Fragment loginFragment = IdpLoginFragment.Companion.newInstance(entry.getKey(), entry.getValue()); - transaction.add(R.id.third_party_auth, loginFragment, authenticationName); - } - } - - // Remove authentication fragments that have been removed from server - for (Fragment fragment: getSupportFragmentManager().getFragments()) { - if (!strategies.keySet().contains(fragment.getTag())) { - transaction.remove(fragment); - } - } - - transaction.commit(); - } - - /** - * Hides keyboard when clicking elsewhere - * - * @param view - */ - private void hideKeyboardOnClick(View view) { - // Set up touch listener for non-text box views to hide keyboard. - if (!(view instanceof EditText) && !(view instanceof Button)) { - view.setOnTouchListener(new OnTouchListener() { - @Override - public boolean onTouch(View v, MotionEvent event) { - InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); - if (getCurrentFocus() != null) { - inputMethodManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); - } - return false; - } - }); - } - - // If a layout container, iterate over children and seed recursion. - if (view instanceof ViewGroup) { - for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) { - View innerView = ((ViewGroup) view).getChildAt(i); - hideKeyboardOnClick(innerView); - } - } - } - - public void changeServerURL() { - Intent intent = new Intent(this, ServerUrlActivity.class); - startActivity(intent); - finish(); - } - - /** - * Handle the Uri used to launch MAGE - * - * @param uri - */ - private void handleUri(Uri uri) { - - // Attempt to get a local file path - String openPath = MediaUtility.getPath(this, uri); - - // If not a local or temporary file path, copy the file to cache - // Cannot pass this to another activity to handle as the URI might - // become invalid between now and then. Copy it now - if (openPath == null || MediaUtility.isTemporaryPath(openPath)) { - CacheUtils.copyToCache(this, uri, openPath); - } else { - // Else, store the path to pass to further intents - mOpenFilePath = openPath; - } - } - - /** - * Fired when user clicks signup - */ - public void signup(View view) { - Intent intent; - if (Compatibility.Companion.isServerVersion5(getApplicationContext())) { - intent = new Intent(getApplicationContext(), SignupActivityServer5.class); - - } else { - intent = new Intent(getApplicationContext(), SignupActivity.class); - } - - startActivity(intent); - finish(); - } - - private void loginComplete(Boolean userChanged) { - boolean preserveActivityStack = !userChanged && mContinueSession; - startNextActivityAndFinish(preserveActivityStack); - } - - public void startNextActivityAndFinish(boolean preserveActivityStack) { - // Continue session if there are other activities on the stack - if (preserveActivityStack && !isTaskRoot()) { - // We are going to return user to the app where they last left off, - // make sure to start up MAGE services - application.onLogin(); - - // TODO look at refreshing the event here... - } else { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - boolean showDisclaimer = sharedPreferences.getBoolean(getString(R.string.serverDisclaimerShow), false); - - Intent intent = showDisclaimer ? - new Intent(getApplicationContext(), DisclaimerActivity.class) : - new Intent(getApplicationContext(), EventsActivity.class); - - // If launched with a local file path, save as an extra - if (mOpenFilePath != null) { - intent.putExtra(LandingActivity.EXTRA_OPEN_FILE_PATH, mOpenFilePath); - } - - startActivity(intent); - } - - finish(); - } - - public void skipLogin() { - Intent intent; - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - boolean disclaimerAccepted = sharedPreferences.getBoolean(getString(R.string.disclaimerAcceptedKey), false); - if (disclaimerAccepted) { - Event event = null; - try { - User user = UserHelper.getInstance(getApplicationContext()).readCurrentUser(); - event = user.getCurrentEvent(); - } catch (UserException e) { - e.printStackTrace(); - } - - intent = event == null ? - new Intent(getApplicationContext(), EventsActivity.class) : - new Intent(getApplicationContext(), LandingActivity.class); - } else { - intent = new Intent(getApplicationContext(), DisclaimerActivity.class); - } - - // If launched with a local file path, save as an extra - if (mOpenFilePath != null) { - intent.putExtra(LandingActivity.EXTRA_OPEN_FILE_PATH, mOpenFilePath); - } - - startActivity(intent); - finish(); - } - - @Override - protected void onResume() { - super.onResume(); - - if (getIntent().getBooleanExtra("LOGOUT", false)) { - application.onLogout(true, null); - } - } - - private void showSessionExpiredDialog() { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle("Session Expired") - .setCancelable(false) - .setMessage("We apologize, but it looks like your MAGE session has expired. Please login and we will take you back to what you were doing.") - .setPositiveButton(android.R.string.ok, null).create(); - - dialog.setCanceledOnTouchOutside(false); - - dialog.show(); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.kt b/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.kt new file mode 100644 index 000000000..d19dc4893 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/login/LoginActivity.kt @@ -0,0 +1,480 @@ +package mil.nga.giat.mage.login + +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Typeface +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager +import android.widget.Button +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.work.WorkManager +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GooglePlayServicesUtil +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.giat.mage.LandingActivity +import mil.nga.giat.mage.MageApplication +import mil.nga.giat.mage.R +import mil.nga.giat.mage.R.xml +import mil.nga.giat.mage.cache.CacheUtils +import mil.nga.giat.mage.compat.server5.login.SignupActivityServer5 +import mil.nga.giat.mage.contact.ContactDialog +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.di.TokenProvider +import mil.nga.giat.mage.disclaimer.DisclaimerActivity +import mil.nga.giat.mage.event.EventsActivity +import mil.nga.giat.mage.login.AuthenticationStatus.AccountCreated +import mil.nga.giat.mage.login.AuthenticationStatus.Offline +import mil.nga.giat.mage.login.AuthorizationStatus.FailAuthentication +import mil.nga.giat.mage.login.AuthorizationStatus.FailAuthorization +import mil.nga.giat.mage.login.AuthorizationStatus.FailInvalidServer +import mil.nga.giat.mage.login.LoginViewModel.Authentication +import mil.nga.giat.mage.login.LoginViewModel.AuthenticationState +import mil.nga.giat.mage.login.LoginViewModel.Authorization +import mil.nga.giat.mage.login.idp.IdpLoginFragment +import mil.nga.giat.mage.login.ldap.LdapLoginFragment +import mil.nga.giat.mage.login.mage.MageLoginFragment +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.sdk.Compatibility.Companion.isServerVersion5 +import mil.nga.giat.mage.sdk.preferences.PreferenceHelper +import mil.nga.giat.mage.sdk.utils.MediaUtility +import org.apache.commons.lang3.StringUtils +import org.json.JSONException +import org.json.JSONObject +import java.util.TreeMap +import javax.inject.Inject + +@AndroidEntryPoint +class LoginActivity : AppCompatActivity() { + @Inject lateinit var application: MageApplication + @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var tokenProvider: TokenProvider + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var cacheProvider: CacheProvider + + private lateinit var viewModel: LoginViewModel + private lateinit var serverUrlText: TextView + + private var mOpenFilePath: String? = null + private var mContinueSession = false + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val intent = intent + mContinueSession = getIntent().getBooleanExtra(EXTRA_CONTINUE_SESSION, false) + val continueSessionWhileUsing = getIntent().getBooleanExtra( + EXTRA_CONTINUE_SESSION_WHILE_USING, false + ) + intent.removeExtra(EXTRA_CONTINUE_SESSION_WHILE_USING) + if (continueSessionWhileUsing && savedInstanceState == null) { + showSessionExpiredDialog() + } + if (intent.getBooleanExtra("LOGOUT", false)) { + application.onLogout(true) + } + + // IMPORTANT: load the configuration from preferences files and server + val preferenceHelper = PreferenceHelper.getInstance(applicationContext) + preferenceHelper.initialize(false, xml::class.java) + + // check if the database needs to be upgraded, and if so log them out + if (MageSqliteOpenHelper.DATABASE_VERSION != preferences.getInt(resources.getString(R.string.databaseVersionKey), 0) + ) { + application.onLogout(true) + } + preferences.edit().putInt(getString(R.string.databaseVersionKey), MageSqliteOpenHelper.DATABASE_VERSION).apply() + + // check google play services version + val isGooglePlayServicesAvailable = GooglePlayServicesUtil.isGooglePlayServicesAvailable( + applicationContext + ) + if (isGooglePlayServicesAvailable != ConnectionResult.SUCCESS) { + if (GooglePlayServicesUtil.isUserRecoverableError(isGooglePlayServicesAvailable)) { + val dialog = GooglePlayServicesUtil.getErrorDialog(isGooglePlayServicesAvailable, this, 1) + dialog?.setOnCancelListener { dialog1: DialogInterface -> + dialog1.dismiss() + finish() + } + dialog?.show() + } else { + AlertDialog.Builder(this).setTitle("Google Play Services") + .setMessage("Google Play Services is not installed, or needs to be updated. Please update Google Play Services before continuing.") + .setPositiveButton( + android.R.string.ok + ) { dialog, _ -> + dialog.dismiss() + finish() + }.show() + } + } + + // Handle when MAGE was launched with a Uri (such as a local or remote cache file) + var uri = intent.data + if (uri == null) { + val bundle = intent.extras + if (bundle != null) { + val objectUri = bundle[Intent.EXTRA_STREAM] + if (objectUri != null) { + uri = objectUri as Uri? + } + } + } + uri?.let { handleUri(it) } + + // if token is not expired, then skip the login module + if (!tokenProvider.isExpired()) { + skipLogin() + } else { + // temporarily prune complete work on every login to ensure our unique work is rescheduled + WorkManager.getInstance(applicationContext).pruneWork() + application.stopLocationService() + } + + // no title bar + setContentView(R.layout.activity_login) + hideKeyboardOnClick(findViewById(R.id.login)) + val appName = findViewById(R.id.mage) + appName.setTypeface(Typeface.createFromAsset(assets, "fonts/GondolaMage-Regular.otf")) + (findViewById(R.id.login_version) as TextView).text = "App Version: " + preferences.getString(getString(R.string.buildVersionKey), "NA") + serverUrlText = findViewById(R.id.server_url) + val serverUrl = preferences.getString(getString(R.string.serverURLKey), getString(R.string.serverURLDefaultValue))!! + if (StringUtils.isEmpty(serverUrl)) { + changeServerURL() + return + } + findViewById(R.id.server_url).setOnClickListener { changeServerURL() } + serverUrlText.text = serverUrl + + // Setup login based on last api pull + configureLogin() + viewModel = ViewModelProvider(this).get(LoginViewModel::class.java) + viewModel.apiStatus.observe(this) { observeApi() } + viewModel.authenticationState.observe(this) { observeAuthenticationState(it) } + viewModel.authenticationStatus.observe(this) { observeAuthentication(it) } + viewModel.authorizationStatus.observe(this) { observeAuthorization(it) } + viewModel.api(serverUrl) + } + + override fun onBackPressed() { + if (mContinueSession) { + // In this case the activity stack was preserved. Don't allow the user to go back to an activity without logging in. + // Since this is the application entry point, assume back means go home. + val intent = Intent(Intent.ACTION_MAIN) + intent.addCategory(Intent.CATEGORY_HOME) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + return + } + super.onBackPressed() + } + + private fun observeApi() { + configureLogin() + } + + private fun observeAuthenticationState(state: AuthenticationState) { + if (state === AuthenticationState.LOADING) { + findViewById(R.id.login_status).visibility = View.VISIBLE + findViewById(R.id.login_form).visibility = View.GONE + } else if (state === AuthenticationState.ERROR) { + findViewById(R.id.login_status).visibility = View.GONE + findViewById(R.id.login_form).visibility = View.VISIBLE + } + } + + private fun observeAuthentication(authentication: Authentication?) { + if (authentication == null) return + val status = authentication.status + if (status is AuthenticationStatus.Success) { + val token = status.token + viewModel.authorize(authentication.strategy, token) + } else if (status is AccountCreated) { + val message = status.message + val dialog = ContactDialog(this, (preferences), "Account Created", message) + dialog.setAuthenticationStrategy(authentication.strategy) + dialog.show(null) + } else if (status is Offline) { + val message = status.message + val dialog = ContactDialog(this, (preferences), "Sign in Failed", message) + dialog.setAuthenticationStrategy(authentication.strategy) + dialog.show { workOffline: Boolean -> + if (workOffline) { + loginComplete(false) + } + viewModel.completeOffline(workOffline) + } + } else if (status is AuthenticationStatus.Failure) { + val message = status.message + val dialog = ContactDialog(this, (preferences), "Sign in Failed", message) + dialog.setAuthenticationStrategy(authentication.strategy) + dialog.show(null) + } + } + + private fun observeAuthorization(authorization: Authorization?) { + if (authorization == null) return + val status = authorization.status + if (status is AuthorizationStatus.Success) { + loginComplete(authorization.userChanged) + } else if (status is FailAuthorization) { + val dialog = ContactDialog( + this, + preferences, + "Registration Sent", + getString(R.string.device_registered_text) + ) + val user = status.user + if (user != null) { + dialog.username = user.username + } + dialog.show(null) + } else if (status is FailInvalidServer) { + val dialog = ContactDialog( + this, + preferences, + "Application Compatibility Error", + "MAGE is not compatible with this server, please ensure your application is up to date or contact your MAGE administrator." + ) + dialog.show(null) + } + if (status is FailAuthentication) { + val message = status.message + val dialog = ContactDialog(this, preferences, "Sign-in Failed", message) + val user = status.user + if (user != null) { + dialog.username = user.username + } + dialog.show(null) + } + } + + private fun configureLogin() { + val transaction = supportFragmentManager.beginTransaction() + val strategies: MutableMap = TreeMap() + + // TODO marshal authentication strategies to POJOs with Jackson + val authenticationStrategies = + PreferenceHelper.getInstance(applicationContext).authenticationStrategies + val iterator = authenticationStrategies.keys() + while (iterator.hasNext()) { + val strategyKey = iterator.next() + try { + val strategy = authenticationStrategies[strategyKey] as JSONObject + if (("local" == strategyKey)) { + strategy.putOpt("type", strategyKey) + } + strategies[strategyKey] = strategy + } catch (e: JSONException) { + Log.e(LOG_NAME, "Error parsing authentication strategy", e) + } + } + if (strategies.size > 1 && strategies.containsKey("local")) { + findViewById(R.id.or).visibility = View.VISIBLE + } else { + findViewById(R.id.or).visibility = View.GONE + } + findViewById(R.id.google_login_button).visibility = View.GONE + for (entry: Map.Entry in strategies.entries) { + val authenticationName = entry.key + val authenticationType = entry.value.optString("type") + if (supportFragmentManager.findFragmentByTag(authenticationName) != null) continue + if (("local" == authenticationName)) { + val loginFragment: Fragment = MageLoginFragment.newInstance( + entry.key!!, entry.value + ) + transaction.add(R.id.local_auth, loginFragment, authenticationName) + } else if (("oauth" == authenticationType)) { + val loginFragment: Fragment = IdpLoginFragment.newInstance( + (entry.key)!!, entry.value + ) + transaction.add(R.id.third_party_auth, loginFragment, authenticationName) + } else if (("saml" == authenticationType)) { + val loginFragment: Fragment = IdpLoginFragment.newInstance( + (entry.key)!!, entry.value + ) + transaction.add(R.id.third_party_auth, loginFragment, authenticationName) + } else if (("ldap" == authenticationType)) { + val loginFragment: Fragment = LdapLoginFragment.newInstance( + (entry.key)!!, entry.value + ) + transaction.add(R.id.third_party_auth, loginFragment, authenticationName) + } else { + val loginFragment: Fragment = IdpLoginFragment.newInstance( + (entry.key)!!, entry.value + ) + transaction.add(R.id.third_party_auth, loginFragment, authenticationName) + } + } + + // Remove authentication fragments that have been removed from server + for (fragment: Fragment in supportFragmentManager.fragments) { + if (!strategies.keys.contains(fragment.tag)) { + transaction.remove(fragment) + } + } + transaction.commit() + } + + /** + * Hides keyboard when clicking elsewhere + * + * @param view + */ + private fun hideKeyboardOnClick(view: View) { + // Set up touch listener for non-text box views to hide keyboard. + if (view !is EditText && view !is Button) { + view.setOnTouchListener { _, _ -> + view.performClick() + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + if (currentFocus != null) { + inputMethodManager.hideSoftInputFromWindow(currentFocus!!.windowToken, 0) + } + false + } + } + + // If a layout container, iterate over children and seed recursion. + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + val innerView = view.getChildAt(i) + hideKeyboardOnClick(innerView) + } + } + } + + fun changeServerURL() { + val intent = Intent(this, ServerUrlActivity::class.java) + startActivity(intent) + finish() + } + + /** + * Handle the Uri used to launch MAGE + * + * @param uri + */ + private fun handleUri(uri: Uri) { + // Attempt to get a local file path + val openPath = MediaUtility.getPath(this, uri) + + // If not a local or temporary file path, copy the file to cache + // Cannot pass this to another activity to handle as the URI might + // become invalid between now and then. Copy it now + if (openPath == null || MediaUtility.isTemporaryPath(openPath)) { + CoroutineScope(Dispatchers.IO).launch { + CacheUtils(applicationContext, cacheProvider).copyToCache(uri, openPath) + } + + } else { + // Else, store the path to pass to further intents + mOpenFilePath = openPath + } + } + + /** + * Fired when user clicks signup + */ + fun signup(view: View?) { + val intent = if (isServerVersion5(applicationContext)) { + Intent(applicationContext, SignupActivityServer5::class.java) + } else { + Intent(applicationContext, SignupActivity::class.java) + } + startActivity(intent) + finish() + } + + private fun loginComplete(userChanged: Boolean) { + val preserveActivityStack = !userChanged && mContinueSession + startNextActivityAndFinish(preserveActivityStack) + } + + private fun startNextActivityAndFinish(preserveActivityStack: Boolean) { + // Continue session if there are other activities on the stack + if (preserveActivityStack && !isTaskRoot) { + // We are going to return user to the app where they last left off, + // make sure to start up MAGE services + application.onLogin() + + // TODO look at refreshing the event here... + } else { + val showDisclaimer = preferences.getBoolean(getString(R.string.serverDisclaimerShow), false) + val intent = if (showDisclaimer) Intent( + applicationContext, + DisclaimerActivity::class.java + ) else Intent( + applicationContext, EventsActivity::class.java + ) + + // If launched with a local file path, save as an extra + if (mOpenFilePath != null) { + intent.putExtra(LandingActivity.EXTRA_OPEN_FILE_PATH, mOpenFilePath) + } + startActivity(intent) + } + finish() + } + + private fun skipLogin() { + val intent: Intent + val disclaimerAccepted = preferences.getBoolean(getString(R.string.disclaimerAcceptedKey), false) + if (disclaimerAccepted) { + var event: Event? = null + val user = userLocalDataSource.readCurrentUser() + if (user != null) { + event = user.currentEvent + } + intent = + if (event == null) Intent(applicationContext, EventsActivity::class.java) else Intent( + applicationContext, LandingActivity::class.java + ) + } else { + intent = Intent(applicationContext, DisclaimerActivity::class.java) + } + + // If launched with a local file path, save as an extra + if (mOpenFilePath != null) { + intent.putExtra(LandingActivity.EXTRA_OPEN_FILE_PATH, mOpenFilePath) + } + startActivity(intent) + finish() + } + + override fun onResume() { + super.onResume() + if (intent.getBooleanExtra("LOGOUT", false)) { + application.onLogout(true) + } + } + + private fun showSessionExpiredDialog() { + val dialog = AlertDialog.Builder(this) + .setTitle("Session Expired") + .setCancelable(false) + .setMessage("We apologize, but it looks like your MAGE session has expired. Please login and we will take you back to what you were doing.") + .setPositiveButton(android.R.string.ok, null).create() + dialog.setCanceledOnTouchOutside(false) + dialog.show() + } + + companion object { + private val LOG_NAME = LoginActivity::class.java.name + + const val EXTRA_CONTINUE_SESSION = "CONTINUE_SESSION" + const val EXTRA_CONTINUE_SESSION_WHILE_USING = "CONTINUE_SESSION_WHILE_USING" + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/login/LoginViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/login/LoginViewModel.kt index a87a0d2c5..6e740e678 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/LoginViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/LoginViewModel.kt @@ -2,37 +2,34 @@ package mil.nga.giat.mage.login import android.app.Application import android.content.SharedPreferences -import android.util.Log import androidx.appcompat.app.AppCompatDelegate import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.user.UserHelper -import mil.nga.giat.mage.sdk.login.AuthenticationStatus -import mil.nga.giat.mage.sdk.login.AuthenticationTask -import mil.nga.giat.mage.sdk.login.AuthorizationStatus -import mil.nga.giat.mage.sdk.login.AuthorizationTask +import mil.nga.giat.mage.data.repository.api.ApiRepository +import mil.nga.giat.mage.data.repository.api.ApiResponse +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.di.TokenProvider +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper import mil.nga.giat.mage.sdk.preferences.PreferenceHelper -import mil.nga.giat.mage.sdk.preferences.ServerApi -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory import mil.nga.giat.mage.sdk.utils.PasswordUtility -import org.apache.commons.lang3.StringUtils -import java.util.* import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( - val application: Application, - val preferences: SharedPreferences + val application: Application, + val preferences: SharedPreferences, + private val daoStore: MageSqliteOpenHelper, + private val tokenProvider: TokenProvider, + private val apiRepository: ApiRepository, + private val userRepository: UserRepository ): ViewModel() { - companion object { - private val LOG_NAME = LoginViewModel::class.java.name - } - data class Authentication(val strategy: String, val status: AuthenticationStatus) data class Authorization(val status: AuthorizationStatus, val userChanged: Boolean = true) @@ -48,96 +45,93 @@ class LoginViewModel @Inject constructor( private val _authenticationStatus = MutableLiveData() val authenticationStatus: LiveData = _authenticationStatus - fun authenticate(strategy: String, credentials: Array, allowDisconnectedLogin: Boolean = false) { - AuthenticationTask(application, allowDisconnectedLogin) { - _authenticationStatus.value = Authentication(strategy, it) + fun authenticate(strategy: String, username: String, password: String) { + _authenticationStatus.value = null + _authenticationState.value = AuthenticationState.LOADING - if (strategy == "local" && it.status == AuthenticationStatus.Status.SUCCESSFUL_AUTHENTICATION) { - localCredentials = credentials - } + viewModelScope.launch(Dispatchers.IO) { + val status = userRepository.authenticateLocal(strategy, username, password) + _authenticationStatus.postValue(Authentication(strategy, status)) - _authenticationState.value = if (it.status != AuthenticationStatus.Status.FAILED_AUTHENTICATION) { - AuthenticationState.SUCCESS - } else { - AuthenticationState.ERROR + if (strategy == "local" && status is AuthenticationStatus.Success) { + localCredentials = arrayOf(username, password) } - }.execute(*credentials, strategy) - _authenticationStatus.value = null - _authenticationState.value = AuthenticationState.LOADING + val state = if (status !is AuthenticationStatus.Failure) { + AuthenticationState.SUCCESS + } else AuthenticationState.ERROR + _authenticationState.postValue(state) + } } private val _authorizationStatus = MutableLiveData() val authorizationStatus: LiveData = _authorizationStatus fun authorize(strategy: String, token: String) { - AuthorizationTask(application) { - if (it.status == AuthorizationStatus.Status.SUCCESSFUL_AUTHORIZATION) { - if ("local" == strategy) { - setupDisconnectedLogin() - } + _authorizationStatus.value = null + _authenticationState.value = AuthenticationState.LOADING - val userChanged = completeAuthorization(strategy, it) - _authorizationStatus.value = Authorization(it, userChanged) + viewModelScope.launch { + when (val status = userRepository.authorize(token)) { + is AuthorizationStatus.Success -> { + if ("local" == strategy) { + setupDisconnectedLogin() + } - } else { - _authorizationStatus.value = Authorization(it) + val userChanged = completeAuthorization(strategy, status) + _authorizationStatus.value = Authorization(status, userChanged) + _authenticationState.value = AuthenticationState.SUCCESS + } + else -> { + _authorizationStatus.value = Authorization(status) + _authenticationState.value = AuthenticationState.ERROR + } } + } + } - _authenticationState.value = if (it.status == AuthorizationStatus.Status.SUCCESSFUL_AUTHORIZATION) { - AuthenticationState.SUCCESS - } else { - AuthenticationState.ERROR - } - }.execute(token) + fun completeOffline(workOffline: Boolean) { + if (workOffline) { + userRepository.authenticateOffline() + } - _authorizationStatus.value = null - _authenticationState.value = AuthenticationState.LOADING + _authenticationState.postValue(AuthenticationState.ERROR) } private val _apiStatus = MutableLiveData() val apiStatus: LiveData = _apiStatus fun api(url: String) { - if (StringUtils.isEmpty(url)) { - return - } - - val serverApi = ServerApi(application) - serverApi.validateServerApi(url) { valid, _ -> - if (authenticationState.value != AuthenticationState.LOADING) { - _apiStatus.value = valid + if (url.isNotEmpty()) { + viewModelScope.launch { + val response = apiRepository.getApi(url) + if (authenticationState.value != AuthenticationState.LOADING) { + _apiStatus.value = response is ApiResponse.Valid + } } } } private fun setupDisconnectedLogin() { - localCredentials?.let { - val username = it[0] - val password = it[1] + localCredentials?.let { (username, password) -> val editor = preferences.edit() editor.putString(application.getString(R.string.usernameKey), username).apply() - try { - val hashedPassword = PasswordUtility.getSaltedHash(password) - editor.putString(application.getString(R.string.passwordHashKey), hashedPassword).commit() - } catch (e: Exception) { - Log.e(LOG_NAME, "Could not hash password", e) - } + val hashedPassword = PasswordUtility.getSaltedHash(password) + editor.putString(application.getString(R.string.passwordHashKey), hashedPassword).commit() } localCredentials = null } - private fun completeAuthorization(strategy: String, status: AuthorizationStatus): Boolean { - val user = status.user + private fun completeAuthorization(strategy: String, status: AuthorizationStatus.Success): Boolean { val previousUser = preferences.getString(application.getString(R.string.sessionUserKey), null) val previousStrategy = preferences.getString(application.getString(R.string.sessionStrategyKey), null) val sessionChanged = (previousStrategy != null && strategy != previousStrategy) || - (previousUser != null && user.username != previousUser) + (previousUser != null && status.user.username != previousUser) if (sessionChanged) { - DaoStore.getInstance(application).resetDatabase() + daoStore.resetDatabase() val preferenceHelper = PreferenceHelper.getInstance(application) preferenceHelper.initialize(true, R.xml::class.java) @@ -146,19 +140,12 @@ class LoginViewModel @Inject constructor( AppCompatDelegate.setDefaultNightMode(dayNightTheme) } - user.fetchedDate = Date() - val userHelper = UserHelper.getInstance(application) - val currentUser = userHelper.createOrUpdate(user) - userHelper.setCurrentUser(currentUser) - - // Successful login, put the token information in the shared preferences - preferences.edit() - .putString(application.getString(R.string.sessionUserKey), user.username) - .putString(application.getString(R.string.sessionStrategyKey), strategy) - .putString(application.getString(R.string.tokenKey), status.token.trim { it <= ' ' }) - .putString(application.getString(R.string.tokenExpirationDateKey), ISO8601DateFormatFactory.ISO8601().format(status.tokenExpiration)) - .putLong(application.getString(R.string.tokenExpirationLengthKey), status.tokenExpiration.time - Date().time) - .commit() + tokenProvider.updateToken( + username = status.user.username, + authenticationStrategy = strategy, + token = status.token.trim(), + expiration = status.tokenExpiration + ) return sessionChanged } diff --git a/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlActivity.java b/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlActivity.java index 8ea4b9073..d93b21b8a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlActivity.java +++ b/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlActivity.java @@ -25,18 +25,25 @@ import java.util.List; import java.util.Locale; +import javax.inject.Inject; + import dagger.hilt.android.AndroidEntryPoint; import mil.nga.giat.mage.R; import mil.nga.giat.mage.contact.ContactDialog; import mil.nga.giat.mage.network.Resource; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.observation.AttachmentHelper; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper; +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper; +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource; +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource; import mil.nga.giat.mage.sdk.preferences.PreferenceHelper; @AndroidEntryPoint public class ServerUrlActivity extends AppCompatActivity { + @Inject + MageSqliteOpenHelper daoStore; + @Inject ObservationLocalDataSource observationLocalDataSource; + @Inject AttachmentLocalDataSource attachmentLocalDataSource; + private View apiStatusView; private View serverUrlForm; private EditText serverUrlTextView; @@ -81,8 +88,8 @@ public void onCreate(Bundle savedInstanceState) { } private void onChangeServerUrl() { - int unsavedObservations = ObservationHelper.getInstance(getApplicationContext()).getDirty().size(); - int unsavedAttachments = AttachmentHelper.getInstance(getApplicationContext()).getDirtyAttachments().size(); + int unsavedObservations = observationLocalDataSource.getDirty().size(); + int unsavedAttachments = attachmentLocalDataSource.getDirtyAttachments().size(); List warnings = new ArrayList<>(); if (unsavedObservations > 0) { @@ -135,7 +142,7 @@ public void onApi(Resource resource) { sharedPreferences, "Compatibility Error", "Your MAGE application is not compatible with this server. Please update your application or contact your MAGE administrator for support."); - dialog.show(); + dialog.show(null); serverUrlLayout.setError("Application is not compatible with server."); } @@ -149,7 +156,7 @@ public void onApi(Resource resource) { } private void done() { - DaoStore.getInstance(getApplicationContext()).resetDatabase(); + daoStore.resetDatabase(); PreferenceHelper preferenceHelper = PreferenceHelper.getInstance(getApplicationContext()); preferenceHelper.initialize(true, R.xml.class); diff --git a/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlViewModel.kt index ff0044451..8be81ff90 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/ServerUrlViewModel.kt @@ -10,35 +10,38 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.MageDatabase +import mil.nga.giat.mage.database.MageDatabase +import mil.nga.giat.mage.data.repository.api.ApiRepository +import mil.nga.giat.mage.data.repository.api.ApiResponse +import mil.nga.giat.mage.database.dao.MageSqliteOpenHelper import mil.nga.giat.mage.network.Resource -import mil.nga.giat.mage.sdk.preferences.ServerApi import javax.inject.Inject @HiltViewModel class ServerUrlViewModel @Inject constructor( - val application: Application, - val preferences: SharedPreferences, - val database: MageDatabase + private val application: Application, + private val preferences: SharedPreferences, + private val daoStore: MageSqliteOpenHelper, + private val database: MageDatabase, + private val apiRepository: ApiRepository ): ViewModel() { - private var serverApi = ServerApi(application) private val _api = MutableLiveData>() val api: LiveData> = _api fun setUrl(url: String) { - _api.value = Resource.loading(null) - serverApi.validateServerApi(url) { valid, error -> - if (valid) { - viewModelScope.launch(Dispatchers.IO) { - database.destroy(application) + viewModelScope.launch(Dispatchers.IO) { + when (val response = apiRepository.getApi(url)) { + is ApiResponse.Valid -> { + daoStore.resetDatabase() + database.destroy() preferences.edit().putString(application.getString(R.string.serverURLKey), url).apply() - _api.postValue(Resource.success(valid)) + _api.postValue(Resource.success(true)) } - } else { - if (error == null) { + is ApiResponse.Invalid -> { _api.postValue(Resource.success(false)) - } else { - _api.postValue(Resource.error(error.cause?.localizedMessage ?: "Cannot connect to server.", false)) + } + is ApiResponse.Error -> { + _api.postValue(Resource.error(response.message, false)) } } } diff --git a/mage/src/main/java/mil/nga/giat/mage/login/SignupViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/login/SignupViewModel.kt index 113d14904..e055bebc3 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/SignupViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/SignupViewModel.kt @@ -1,23 +1,20 @@ package mil.nga.giat.mage.login -import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.google.gson.JsonObject import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import mil.nga.giat.mage.sdk.http.resource.UserResource -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import kotlinx.coroutines.launch +import mil.nga.giat.mage.data.repository.user.UserRepository import javax.inject.Inject @HiltViewModel open class SignupViewModel @Inject constructor( - @ApplicationContext val context: Context, - val preferences: SharedPreferences + val preferences: SharedPreferences, + private val userRepository: UserRepository ): ViewModel() { enum class CaptchaState { @@ -32,7 +29,6 @@ open class SignupViewModel @Inject constructor( INVALID_CAPTCHA, INVALID_USERNAME } - private val userResource = UserResource(context) private var username = "" private var backgroundColor = "#FFFFFF" @@ -58,9 +54,11 @@ open class SignupViewModel @Inject constructor( fun getCaptcha(username: String, backgroundColor: String) { this.username = username this.backgroundColor = backgroundColor + _captchaState.value = CaptchaState.LOADING - userResource.getCaptcha(username, backgroundColor, object: Callback { - override fun onResponse(call: Call, response: Response) { + viewModelScope.launch { + try { + val response = userRepository.getCaptcha(username, backgroundColor) if (response.isSuccessful) { val json = response.body()!! captchaToken = json.get("token").asString @@ -68,20 +66,16 @@ open class SignupViewModel @Inject constructor( } _captchaState.value = CaptchaState.COMPLETE - } - - override fun onFailure(call: Call, t: Throwable) { + } catch (e: Exception) { _captchaState.value = CaptchaState.COMPLETE } - }) - - _captchaState.value = CaptchaState.LOADING + } } open fun signup(account: Account, captchaText: String) { - val userResource = UserResource(context) - userResource.verifyUser(account.displayName, account.email, account.phone, account.password, captchaText, captchaToken, object: Callback { - override fun onResponse(call: Call, response: Response) { + viewModelScope.launch { + val response = userRepository.verifyUser(account.displayName, account.email, account.phone, account.password, captchaText, captchaToken) + try { if (response.isSuccessful) { _signupStatus.value = SignupStatus(true, response.body()) } else { @@ -96,15 +90,13 @@ open class SignupViewModel @Inject constructor( } _signupState.value = SignupState.COMPLETE + } catch (e: Exception) { + _signupStatus.value = SignupStatus(false, null, null, e.localizedMessage, account.username) } - override fun onFailure(call: Call, t: Throwable) { - _signupStatus.value = SignupStatus(false, null, null, t.localizedMessage, account.username) - } - }) - - _signupStatus.value = null - _signupState.value = SignupState.LOADING + _signupStatus.value = null + _signupState.value = SignupState.LOADING + } } fun cancel() { diff --git a/mage/src/main/java/mil/nga/giat/mage/login/ldap/LdapLoginFragment.kt b/mage/src/main/java/mil/nga/giat/mage/login/ldap/LdapLoginFragment.kt index 16e827f55..b2fc1ddda 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/ldap/LdapLoginFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/ldap/LdapLoginFragment.kt @@ -94,7 +94,7 @@ class LdapLoginFragment: Fragment() { return } - viewModel.authenticate(strategyName, arrayOf(username, password)) + viewModel.authenticate(strategyName, username, password) } private fun observeLogin(authentication: LoginViewModel.Authentication?) { diff --git a/mage/src/main/java/mil/nga/giat/mage/login/mage/MageLoginFragment.kt b/mage/src/main/java/mil/nga/giat/mage/login/mage/MageLoginFragment.kt index cc1835ba4..f74d1e91f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/login/mage/MageLoginFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/login/mage/MageLoginFragment.kt @@ -12,7 +12,7 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.databinding.FragmentAuthenticationMageBinding import mil.nga.giat.mage.login.LoginViewModel @@ -20,12 +20,8 @@ import org.json.JSONObject import java.util.* import javax.inject.Inject -/** - * Created by wnewman on 1/3/18. - */ - @AndroidEntryPoint -class MageLoginFragment : Fragment() { +open class MageLoginFragment : Fragment() { companion object { private const val EXTRA_LOCAL_STRATEGY = "EXTRA_LOCAL_STRATEGY" @@ -107,7 +103,8 @@ class MageLoginFragment : Fragment() { super.onActivityCreated(savedInstanceState) viewModel = activity?.run { - ViewModelProviders.of(this).get(LoginViewModel::class.java) + ViewModelProvider(this).get(LoginViewModel::class.java) + } ?: throw Exception("Invalid Activity") viewModel.authenticationStatus.observe(viewLifecycleOwner, Observer { observeLogin(it) }) @@ -136,7 +133,7 @@ class MageLoginFragment : Fragment() { return } - viewModel.authenticate(strategyName, arrayOf(username.toLowerCase(Locale.getDefault()), password), true) + viewModel.authenticate(strategyName, username.lowercase(Locale.getDefault()), password) } private fun observeLogin(authentication: LoginViewModel.Authentication?) { diff --git a/mage/src/main/java/mil/nga/giat/mage/map/Geocoder.kt b/mage/src/main/java/mil/nga/giat/mage/map/Geocoder.kt index a75793c12..fa55d1edb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/Geocoder.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/Geocoder.kt @@ -17,9 +17,17 @@ import javax.inject.Inject class Geocoder @Inject constructor( private val application: Application ) { - data class SearchResult(val markerOptions: MarkerOptions, val zoom: Int) + sealed class SearchResponse { + data class Success(val result: SearchResult): SearchResponse() + data class Error(val message: String): SearchResponse() + } + + data class SearchResult( + val markerOptions: MarkerOptions, + val zoom: Int, + ) - suspend fun search(text: String): SearchResult? = withContext(Dispatchers.IO) { + suspend fun search(text: String) = withContext(Dispatchers.IO) { if (MGRS.isMGRS(text)) { try { val point = MGRS.parse(text).toPoint() @@ -28,11 +36,12 @@ class Geocoder @Inject constructor( .title(CoordinateSystem.MGRS.name) .snippet(text) - SearchResult(options, 18) + SearchResponse.Success(SearchResult(options, 18)) } catch (ignore: ParseException) { - null + SearchResponse.Error("Failed parsing MGRS text.") } - } else if (GARS.isGARS(text)) { + } + else if (GARS.isGARS(text)) { try { val point = GARS.parse(text).toPoint() val options = MarkerOptions() @@ -40,9 +49,9 @@ class Geocoder @Inject constructor( .title(CoordinateSystem.GARS.name) .snippet(text) - SearchResult(options, 18) + SearchResponse.Success(SearchResult(options, 18)) } catch (ignore: ParseException) { - null + SearchResponse.Error("Failed parsing GARS text.") } } else { val geocoder = Geocoder(application) @@ -59,11 +68,11 @@ class Geocoder @Inject constructor( val zoom = MAX_ADDRESS_ZOOM - (MAX_ADDRESS_LINES - addressLines) * 2 - SearchResult(markerOptions, zoom) - } + SearchResponse.Success(SearchResult(markerOptions, zoom)) + } ?: SearchResponse.Error("Address or location not found.") } catch (e: IOException) { Log.e(LOG_NAME, "Problem executing search.", e) - null + SearchResponse.Error("Address not found, please check network connectivity.") } } } diff --git a/mage/src/main/java/mil/nga/giat/mage/map/MapFragment.kt b/mage/src/main/java/mil/nga/giat/mage/map/MapFragment.kt index f37182b02..1eeb47f39 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/MapFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/MapFragment.kt @@ -64,7 +64,7 @@ import mil.nga.giat.mage.geopackage.media.GeoPackageMediaActivity import mil.nga.giat.mage.glide.transform.LocationAgeTransformation import mil.nga.giat.mage.location.LocationAccess import mil.nga.giat.mage.location.LocationPolicy -import mil.nga.giat.mage.map.Geocoder.SearchResult +import mil.nga.giat.mage.map.Geocoder.SearchResponse import mil.nga.giat.mage.map.MapViewModel.FeedState import mil.nga.giat.mage.map.annotation.MapAnnotation import mil.nga.giat.mage.map.cache.* @@ -79,13 +79,11 @@ import mil.nga.giat.mage.observation.ObservationLocation import mil.nga.giat.mage.observation.edit.ObservationEditActivity import mil.nga.giat.mage.observation.view.ObservationViewActivity import mil.nga.giat.mage.profile.ProfileActivity -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper -import mil.nga.giat.mage.sdk.exceptions.LayerException +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.exceptions.UserException import mil.nga.giat.mage.utils.googleMapsUri import mil.nga.mgrs.MGRS @@ -119,6 +117,11 @@ class MapFragment : Fragment(), @Inject lateinit var preferences: SharedPreferences @Inject lateinit var locationAccess: LocationAccess @Inject lateinit var locationPolicy: LocationPolicy + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var layerLocalDataSource: LayerLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + @Inject lateinit var locationLocalDataSource: LocationLocalDataSource + @Inject lateinit var cacheProvider: CacheProvider private lateinit var binding: FragmentMapBinding @@ -570,9 +573,7 @@ class MapFragment : Fragment(), } } - override fun onChanged(location: android.location.Location?) { - if (location == null) return - + override fun onChanged(location: android.location.Location) { locationChangedListener?.onLocationChanged(location) straightLineNavigation?.updateUserLocation(location) @@ -643,7 +644,7 @@ class MapFragment : Fragment(), val showTraffic = preferences.getBoolean(resources.getString(R.string.showTrafficKey), resources.getBoolean(R.bool.showTrafficDefaultValue)) googleMap.isTrafficEnabled = showTraffic - val currentEvent = EventHelper.getInstance(activity).currentEvent + val currentEvent = eventLocalDataSource.currentEvent var currentEventId = currentEventId if (currentEvent != null) { currentEventId = currentEvent.id @@ -655,7 +656,7 @@ class MapFragment : Fragment(), locations?.clear() } - CacheProvider.getInstance(context).registerCacheOverlayListener(this) + cacheProvider.registerCacheOverlayListener(this) binding.zoomButton.setOnClickListener { zoomToLocation() } @@ -688,47 +689,45 @@ class MapFragment : Fragment(), } } - private fun onSearchResult(result: SearchResult?) { + private fun onSearchResult(response: SearchResponse) { searchMarker?.remove() - if (result == null) { - // TODO See what google gives me, would be nice not to check connectivity - if (ConnectivityUtility.isOnline(context)) { - Toast.makeText(context, "Could not find address.", Toast.LENGTH_LONG).show() - } else { - Toast.makeText(context, "No connectivity, try again later.", Toast.LENGTH_LONG).show() - } - } else { - searchMarker = map?.addMarker(result.markerOptions) - searchMarker?.showInfoWindow() + when (response) { + is SearchResponse.Success -> { + searchMarker = map?.addMarker(response.result.markerOptions) + searchMarker?.showInfoWindow() - var zoom = result.zoom - - val title = result.markerOptions.title - if (title.equals(CoordinateSystem.MGRS.name)) { - val gridType = MGRS.precision(result.markerOptions.snippet) - val grid = mgrsTileProvider.getGrid(gridType) - if (grid != null) { - val maxZoom : Int = if (gridType == GridType.GZD) { - mgrsTileProvider.getGrid(GridType.HUNDRED_KILOMETER).minZoom.minus(1) - } else { - grid.linesMaxZoom + var zoom = response.result.zoom + + val title = response.result.markerOptions.title + if (title.equals(CoordinateSystem.MGRS.name)) { + val gridType = MGRS.precision(response.result.markerOptions.snippet) + val grid = mgrsTileProvider.getGrid(gridType) + if (grid != null) { + val maxZoom : Int = if (gridType == GridType.GZD) { + mgrsTileProvider.getGrid(GridType.HUNDRED_KILOMETER).minZoom.minus(1) + } else { + grid.linesMaxZoom + } + zoom = zoomLevel(zoom, grid.linesMinZoom, maxZoom) + } + } else if (title.equals(CoordinateSystem.GARS.name)) { + val gridType = GARS.precision(response.result.markerOptions.snippet) + val grid = garsTileProvider.getGrid(gridType) + if (grid != null) { + zoom = zoomLevel(zoom, grid.linesMinZoom, grid.linesMaxZoom) } - zoom = zoomLevel(zoom, grid.linesMinZoom, maxZoom) - } - } else if (title.equals(CoordinateSystem.GARS.name)) { - val gridType = GARS.precision(result.markerOptions.snippet) - val grid = garsTileProvider.getGrid(gridType) - if (grid != null) { - zoom = zoomLevel(zoom, grid.linesMinZoom, grid.linesMaxZoom) } - } - val position = CameraPosition.builder() - .target(result.markerOptions.position) - .zoom(zoom.toFloat()).build() + val position = CameraPosition.builder() + .target(response.result.markerOptions.position) + .zoom(zoom.toFloat()).build() - map?.animateCamera(CameraUpdateFactory.newCameraPosition(position)) + map?.animateCamera(CameraUpdateFactory.newCameraPosition(position)) + } + is SearchResponse.Error -> { + Toast.makeText(context, response.message, Toast.LENGTH_LONG).show() + } } } @@ -774,7 +773,7 @@ class MapFragment : Fragment(), private fun updateReportLocationButton() { val serverLocationServiceDisabled = preferences.getBoolean("gLocationServiceDisabled", false) - val memberOfEvent = UserHelper.getInstance(application).isCurrentUserPartOfCurrentEvent + val memberOfEvent = userLocalDataSource.isCurrentUserPartOfCurrentEvent() binding.preciseLocationDenied.visibility = View.GONE if (serverLocationServiceDisabled || !memberOfEvent) { @@ -836,7 +835,7 @@ class MapFragment : Fragment(), } private fun onToggleReportLocation() { - if (!UserHelper.getInstance(application).isCurrentUserPartOfCurrentEvent) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { AlertDialog.Builder(requireActivity()) .setTitle(application.resources.getString(R.string.no_event_title)) .setMessage(application.resources.getString(R.string.location_no_event_message)) @@ -948,7 +947,7 @@ class MapFragment : Fragment(), toggleCoordinateTarget() try { - currentUser = UserHelper.getInstance(application).readCurrentUser() + currentUser = userLocalDataSource.readCurrentUser() } catch (ue: UserException) { Log.e(LOG_NAME, "Could not find current user.", ue) } @@ -957,16 +956,10 @@ class MapFragment : Fragment(), showMgrs = preferences.getBoolean(resources.getString(R.string.showMGRSKey), false) showGars = preferences.getBoolean(resources.getString(R.string.showGARSKey), false) - try { - val event = EventHelper.getInstance(application).currentEvent - val available = LayerHelper.getInstance(application).readByEvent(event, null).any { - !it.isLoaded - } + val event = eventLocalDataSource.currentEvent + val available = layerLocalDataSource.readByEvent(event).any { !it.isLoaded } - binding.availableLayerDownloads.visibility = if (available) View.VISIBLE else View.GONE - } catch (e: LayerException) { - Log.e(LOG_NAME, "Error reading layers", e) - } + binding.availableLayerDownloads.visibility = if (available) View.VISIBLE else View.GONE binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(text: String): Boolean { @@ -1001,7 +994,7 @@ class MapFragment : Fragment(), it.removeObservers(viewLifecycleOwner) } - CacheProvider.getInstance(application).unregisterCacheOverlayListener(this) + cacheProvider.unregisterCacheOverlayListener(this) map?.let { saveMapView() @@ -1057,9 +1050,11 @@ class MapFragment : Fragment(), private fun onNewObservation() { var location: ObservationLocation? = null + val user = userLocalDataSource.readCurrentUser() ?: return + // if there is not a location from the location service, then try to pull one from the database. if (locationProvider?.value == null) { - val locations = LocationHelper.getInstance(application).getCurrentUserLocations(1, true) + val locations = locationLocalDataSource.getCurrentUserLocations(user, 1, true) val userLocation = locations.firstOrNull() if (userLocation != null) { val propertiesMap = userLocation.propertiesMap @@ -1072,7 +1067,7 @@ class MapFragment : Fragment(), location = ObservationLocation(locationProvider?.value) } - if (!UserHelper.getInstance(application).isCurrentUserPartOfCurrentEvent) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { AlertDialog.Builder(requireActivity()) .setTitle(application.resources.getString(R.string.no_event_title)) .setMessage(application.resources.getString(R.string.observation_no_event_message)) @@ -1226,7 +1221,7 @@ class MapFragment : Fragment(), private fun onMapLongClick(point: LatLng) { hideKeyboard() - if (!UserHelper.getInstance(application).isCurrentUserPartOfCurrentEvent) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { AlertDialog.Builder(requireActivity()) .setTitle(application.resources.getString(R.string.no_event_title)) .setMessage(application.resources.getString(R.string.observation_no_event_message)) @@ -1299,8 +1294,10 @@ class MapFragment : Fragment(), override fun onCacheOverlay(overlays: List) { // Add all overlays that are in the preferences - val currentEvent = EventHelper.getInstance(activity).currentEvent - val cacheOverlays = CacheOverlayFilter(application, currentEvent).filter(overlays) + val currentEvent = eventLocalDataSource.currentEvent + val layers = layerLocalDataSource.readByEvent(currentEvent, "GeoPackage"); + + val cacheOverlays = CacheOverlayFilter(application, layers).filter(overlays) // Track enabled cache overlays val enabledCacheOverlays: MutableMap = HashMap() diff --git a/mage/src/main/java/mil/nga/giat/mage/map/MapViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/map/MapViewModel.kt index cdf9ee844..8810f6a30 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/MapViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/MapViewModel.kt @@ -8,28 +8,27 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch -import mil.nga.giat.mage.data.feed.Feed -import mil.nga.giat.mage.data.feed.FeedItemDao -import mil.nga.giat.mage.data.feed.FeedWithItems -import mil.nga.giat.mage.data.feed.ItemWithFeed -import mil.nga.giat.mage.data.layer.LayerRepository -import mil.nga.giat.mage.data.location.LocationRepository -import mil.nga.giat.mage.data.observation.ObservationRepository -import mil.nga.giat.mage.form.Form +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.dao.feed.FeedItemDao +import mil.nga.giat.mage.database.model.feed.FeedWithItems +import mil.nga.giat.mage.database.model.feed.ItemWithFeed +import mil.nga.giat.mage.data.repository.layer.LayerRepository +import mil.nga.giat.mage.data.repository.location.LocationRepository +import mil.nga.giat.mage.data.repository.observation.ObservationRepository import mil.nga.giat.mage.glide.model.Avatar import mil.nga.giat.mage.map.annotation.MapAnnotation import mil.nga.giat.mage.map.preference.MapLayerPreferences import mil.nga.giat.mage.network.Server import mil.nga.giat.mage.network.gson.asStringOrNull import mil.nga.giat.mage.observation.ObservationImportantState -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.exceptions.ObservationException import mil.nga.giat.mage.sdk.exceptions.UserException import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory @@ -44,24 +43,41 @@ data class StaticFeatureId(val layerId: Long, val featureId: Long) @HiltViewModel class MapViewModel @Inject constructor( - private val application: Application, - private val mapLayerPreferences: MapLayerPreferences, - private val feedItemDao: FeedItemDao, - private val geocoder: Geocoder, - private val layerRepository: LayerRepository, - locationRepository: LocationRepository, - observationRepository: ObservationRepository, + private val application: Application, + private val mapLayerPreferences: MapLayerPreferences, + private val feedItemDao: FeedItemDao, + private val geocoder: Geocoder, + private val layerRepository: LayerRepository, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource, + private val locationLocalDataSource: LocationLocalDataSource, + locationRepository: LocationRepository, + observationRepository: ObservationRepository, ): ViewModel() { var dateFormat: DateFormat = DateFormatFactory.format("yyyy-MM-dd HH:mm zz", Locale.getDefault(), application) - private val eventHelper: EventHelper = EventHelper.getInstance(application) private val eventId = MutableLiveData() val observations = observationRepository.getObservations().transform { observations -> - val states = observations.map { observation -> - MapAnnotation.fromObservation(observation, application) - } + val states = eventLocalDataSource.currentEvent?.let { event -> + observations.map { observation -> + val observationForm = observation.forms.firstOrNull() + val formDefinition = observationForm?.formId?.let { formId -> + eventLocalDataSource.getForm(formId) + } + + MapAnnotation.fromObservation( + event = event, + observation = observation, + formDefinition = formDefinition, + observationForm = observationForm, + geometryType = observation.geometry.geometryType, + context = application + ) + } + } ?: emptyList() emit(states) @@ -100,7 +116,7 @@ class MapViewModel @Inject constructor( } val items: LiveData>> = - Transformations.switchMap(feedIds) { feedIds -> + feedIds.switchMap { feedIds -> val items = mutableMapOf>() feedIds.forEach { feedId -> var liveData = _feeds.value?.get(feedId) @@ -126,7 +142,7 @@ class MapViewModel @Inject constructor( } private val searchText = MutableLiveData() - val searchResult = Transformations.switchMap(searchText) { + val searchResult = searchText.switchMap { liveData { emit(geocoder.search(it)) } @@ -140,7 +156,7 @@ class MapViewModel @Inject constructor( val observationMap: LiveData = observationId.switchMap { id -> liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { if (id != null) { - val observation = ObservationHelper.getInstance(application).read(id) + val observation = observationLocalDataSource.read(id) emit(toObservationItemState(observation)) } else { emit(null) @@ -153,7 +169,7 @@ class MapViewModel @Inject constructor( } private fun toObservationItemState(observation: Observation): ObservationMapState { - val currentUser = UserHelper.getInstance(application).readCurrentUser() + val currentUser = userLocalDataSource.readCurrentUser() val isFavorite = if (currentUser != null) { val favorite = observation.favoritesMap[currentUser.remoteId] favorite != null && favorite.isFavorite @@ -161,10 +177,8 @@ class MapViewModel @Inject constructor( val importantState = if (observation.important?.isImportant == true) { val importantUser: User? = try { - UserHelper.getInstance(application).read(observation.important?.userId) - } catch (ue: UserException) { - null - } + observation.important?.userId?.let { userLocalDataSource.read(it) } + } catch (ue: UserException) { null } ObservationImportantState( description = observation.important?.description, @@ -174,19 +188,28 @@ class MapViewModel @Inject constructor( var primary: String? = null var secondary: String? = null - if (observation.forms.isNotEmpty()) { - val observationForm = observation.forms.first() - val formJson = eventHelper.getForm(observationForm.formId).json - val formDefinition = Form.fromJson(formJson) + val observationForm = observation.forms.firstOrNull() + val formDefinition = observationForm?.formId?.let { + eventLocalDataSource.getForm(it) + } + if (observation.forms.isNotEmpty()) { primary = observationForm?.properties?.find { it.key == formDefinition?.primaryMapField }?.value as? String secondary = observationForm?.properties?.find { it.key == formDefinition?.secondaryMapField }?.value as? String } - val iconFeature = MapAnnotation.fromObservation(observation, application) + val event = eventLocalDataSource.currentEvent + val iconFeature = MapAnnotation.fromObservation( + event = event, + observation = observation, + formDefinition = formDefinition, + observationForm = observationForm, + geometryType = observation.geometry.geometryType, + context = application + ) - val user = UserHelper.getInstance(application).read(observation.userId) - val title = "${user.displayName} \u2022 ${dateFormat.format(observation.timestamp)}" + val user = userLocalDataSource.read(observation.userId) + val title = "${user?.displayName} \u2022 ${dateFormat.format(observation.timestamp)}" return ObservationMapState( observation.id, @@ -202,19 +225,14 @@ class MapViewModel @Inject constructor( fun toggleFavorite(observationMapState: ObservationMapState) { viewModelScope.launch(Dispatchers.IO) { - val observationHelper = ObservationHelper.getInstance(application) - val observation = observationHelper.read(observationMapState.id) + val observation = observationLocalDataSource.read(observationMapState.id) try { - val user: User? = try { - UserHelper.getInstance(application).readCurrentUser() - } catch (e: Exception) { - null - } - - if (observationMapState.favorite) { - observationHelper.unfavoriteObservation(observation, user) - } else { - observationHelper.favoriteObservation(observation, user) + userLocalDataSource.readCurrentUser()?.let { user -> + if (observationMapState.favorite) { + observationLocalDataSource.unfavoriteObservation(observation, user) + } else { + observationLocalDataSource.favoriteObservation(observation, user) + } } observationId.value = observation.id @@ -226,7 +244,7 @@ class MapViewModel @Inject constructor( val location = locationId.switchMap { id -> liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) { if (id != null) { - val location = LocationHelper.getInstance(application).read(id) + val location = locationLocalDataSource.read(id) emit(toUserState(location.user, location)) } else { emit(null) @@ -294,8 +312,8 @@ class MapViewModel @Inject constructor( val userPhone: LiveData = _userPhone fun selectUserPhone(userState: UserMapState?) { - val user: User? = userState?.let { - LocationHelper.getInstance(application).read(it.id)?.user + val user = userState?.let { + locationLocalDataSource.read(it.id).user } _userPhone.value = user @@ -305,8 +323,8 @@ class MapViewModel @Inject constructor( val staticFeature: LiveData = _staticFeatureId.switchMap { id -> liveData { if (id != null) { - layerRepository.getStaticFeature(id.layerId, id.featureId)?.let { feature -> - emit(staticFeatureToState(feature)) + layerRepository.getStaticFeature(id.layerId, id.featureId)?.let { + emit(staticFeatureToState(it)) } } else { emit(null) diff --git a/mage/src/main/java/mil/nga/giat/mage/map/annotation/AnnotationStyle.kt b/mage/src/main/java/mil/nga/giat/mage/map/annotation/AnnotationStyle.kt index 2b147a6cf..57756e0a1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/annotation/AnnotationStyle.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/annotation/AnnotationStyle.kt @@ -1,22 +1,32 @@ package mil.nga.giat.mage.map.annotation import android.content.Context -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature +import android.net.Uri +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.observation.ObservationForm import mil.nga.sf.GeometryType +import java.io.File /** * Map annotation style for points, lines and polygons **/ sealed class AnnotationStyle { companion object { - fun fromObservation(observation: Observation, context: Context): AnnotationStyle { - return when (observation.geometry.geometryType) { + fun fromObservation( + event: Event?, + formDefinition: Form?, + observationForm: ObservationForm?, + geometryType: GeometryType, + context: Context + ): AnnotationStyle { + return when (geometryType) { GeometryType.POINT -> { - IconStyle.fromObservation(observation, context) + ObservationIconStyle.fromObservation(event, formDefinition, observationForm, context) } else -> { - ShapeStyle.fromObservation(observation, context) + ShapeStyle.fromObservation(event, formDefinition, observationForm, context) } } } @@ -24,7 +34,14 @@ sealed class AnnotationStyle { fun fromStaticFeature(feature: StaticFeature, context: Context): AnnotationStyle { return when (feature.geometry.geometryType) { GeometryType.POINT -> { - IconStyle.fromStaticFeature(feature) + val iconUri = feature.localPath?.let { path -> + val file = File(path) + if (file.exists()) { + Uri.fromFile(file) + } else null + } + + return IconStyle(iconUri) } else -> { ShapeStyle.fromStaticFeature(feature, context) diff --git a/mage/src/main/java/mil/nga/giat/mage/map/annotation/IconStyle.kt b/mage/src/main/java/mil/nga/giat/mage/map/annotation/IconStyle.kt index 7e88d83b4..1ae044d48 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/annotation/IconStyle.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/annotation/IconStyle.kt @@ -2,68 +2,104 @@ package mil.nga.giat.mage.map.annotation import android.content.Context import android.net.Uri -import mil.nga.giat.mage.data.event.EventRepository -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.data.repository.event.EventRepository +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.observation.ObservationForm import java.io.File import java.io.FileFilter import java.util.* -class IconStyle( +open class IconStyle( val uri: Uri? = null ): AnnotationStyle() { - companion object { - fun fromObservation(observation: Observation, context: Context): IconStyle { - val iconUri = observationIcon(observation, context)?.let { file -> - Uri.fromFile(file) + private val fileFilter = + FileFilter { path: File -> + path.isFile && path.name.startsWith("icon.") } - return IconStyle(iconUri) + fun recurseIconPath(iconProperties: Stack, file: File, index: Int): File? { + var path: File? = file + var i = index + if (iconProperties.size > 0) { + val property = iconProperties.pop() + if (property != null && path?.exists() == true) { + if (property.trim().isNotEmpty() && File(path, property).exists()) { + return recurseIconPath(iconProperties, File(path, property), i + 1) + } + } + } + + while (path?.listFiles(fileFilter) != null && path.listFiles(fileFilter)?.size == 0 && i >= 0) { + path = path.parentFile + i-- + } + + if (path == null || !path.exists()) return null + + val files = path.listFiles(fileFilter) + return if (files?.isNotEmpty() == true) { + files[0] + } else null } + } +} - fun fromObservationProperties(eventId: String, formId: Long?, primary: String?, secondary: String?, context: Context): IconStyle { - val iconUri = observationIcon(eventId, formId, primary, secondary, context)?.let { file -> +class ObservationIconStyle: IconStyle() { + companion object { + fun fromObservation( + event: Event?, + formDefinition: Form?, + observationForm: ObservationForm?, + context: Context + ): IconStyle { + val iconUri = observationIcon( + event, + formDefinition, + observationForm, + context + )?.let { file -> Uri.fromFile(file) } return IconStyle(iconUri) } - fun fromStaticFeature(feature: StaticFeature): IconStyle { - val iconUri = feature.localPath?.let { path -> - val file = File(path) - if (file.exists()) { + fun fromObservationProperties(eventId: String, formId: Long?, primary: String?, secondary: String?, context: Context): IconStyle { + val iconUri = observationIcon(eventId, formId, primary, secondary, context) + ?.let { file -> Uri.fromFile(file) - } else null - } + } return IconStyle(iconUri) } - private fun observationIcon(observation: Observation, context: Context): File? { - val path = File(File(File(context.filesDir.absolutePath + EventRepository.OBSERVATION_ICON_PATH), observation.event.remoteId), "icons") - val iconProperties = Stack() + private fun observationIcon( + event: Event?, + formDefinition: Form?, + observationForm: ObservationForm?, + context: Context + ): File? { + event ?: return null - observation.forms.firstOrNull()?.let { observationForm -> - val form = EventHelper.getInstance(context).getForm(observationForm.formId) + val path = File(File(File(context.filesDir.absolutePath + EventRepository.OBSERVATION_ICON_PATH), event.remoteId), "icons") + val iconProperties = Stack() - form.secondaryMapField?.let { field -> - observationForm.properties.find { it.key == field }?.value?.toString()?.let { - iconProperties.add(it) - } + formDefinition?.secondaryMapField?.let { field -> + observationForm?.properties?.find { it.key == field }?.value?.toString()?.let { + iconProperties.add(it) } + } - form.primaryMapField?.let { field -> - observationForm.properties.find { it.key == field }?.value?.toString()?.let { - iconProperties.add(it) - } + formDefinition?.primaryMapField?.let { field -> + observationForm?.properties?.find { it.key == field }?.value?.toString()?.let { + iconProperties.add(it) } - - iconProperties.add(observationForm.formId.toString()) } + observationForm?.formId?.let { iconProperties.add(it.toString()) } + return recurseIconPath(iconProperties, path, 0) } @@ -85,35 +121,5 @@ class IconStyle( return recurseIconPath(iconProperties, path, 0) } - - private val fileFilter = - FileFilter { path: File -> - path.isFile && path.name.startsWith("icon.") - } - - private fun recurseIconPath(iconProperties: Stack, file: File, index: Int): File? { - var path: File? = file - var i = index - if (iconProperties.size > 0) { - val property = iconProperties.pop() - if (property != null && path?.exists() == true) { - if (property.trim().isNotEmpty() && File(path, property).exists()) { - return recurseIconPath(iconProperties, File(path, property), i + 1) - } - } - } - - while (path?.listFiles(fileFilter) != null && path.listFiles(fileFilter)?.size == 0 && i >= 0) { - path = path.parentFile - i-- - } - - if (path == null || !path.exists()) return null - - val files = path.listFiles(fileFilter) - return if (files?.isNotEmpty() == true) { - files[0] - } else null - } } -} \ No newline at end of file +} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/annotation/MapAnnotation.kt b/mage/src/main/java/mil/nga/giat/mage/map/annotation/MapAnnotation.kt index dd344e55d..1f3a80738 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/annotation/MapAnnotation.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/annotation/MapAnnotation.kt @@ -4,13 +4,17 @@ import android.content.Context import android.graphics.Bitmap import android.net.Uri import com.bumptech.glide.load.Transformation -import mil.nga.giat.mage.data.feed.ItemWithFeed +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.feed.ItemWithFeed import mil.nga.giat.mage.network.Server -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature -import mil.nga.giat.mage.sdk.datastore.user.User +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.database.model.user.User import mil.nga.sf.Geometry +import mil.nga.sf.GeometryType import java.io.File data class MapAnnotation( @@ -41,18 +45,32 @@ data class MapAnnotation( geometry = geometry, timestamp = timestamp, accuracy = accuracy, - style = IconStyle.fromObservationProperties(eventId, formId, primary, secondary, context) + style = ObservationIconStyle.fromObservationProperties(eventId, formId, primary, secondary, context) ) } - fun fromObservation(observation: Observation, context: Context): MapAnnotation { + fun fromObservation( + event: Event?, + observation: Observation, + formDefinition: Form?, + observationForm: ObservationForm?, + geometryType: GeometryType, + context: Context + ): MapAnnotation { + val style = AnnotationStyle.fromObservation( + event = event, + formDefinition = formDefinition, + observationForm = observationForm, + geometryType = geometryType, + context = context) + return MapAnnotation( id = observation.id, layer = "observation", geometry = observation.geometry, timestamp = observation.timestamp.time, accuracy = observation.accuracy, - style = AnnotationStyle.fromObservation(observation, context) + style = style ) } diff --git a/mage/src/main/java/mil/nga/giat/mage/map/annotation/ShapeStyle.kt b/mage/src/main/java/mil/nga/giat/mage/map/annotation/ShapeStyle.kt index 3cf5749bc..c679ddfb5 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/annotation/ShapeStyle.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/annotation/ShapeStyle.kt @@ -11,12 +11,13 @@ import androidx.core.graphics.red import com.google.gson.JsonObject import com.google.gson.JsonParser import mil.nga.giat.mage.R +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.FieldValue import mil.nga.giat.mage.network.gson.asJsonObjectOrNull -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.database.model.feature.StaticFeature +import mil.nga.giat.mage.database.model.event.Form +import mil.nga.giat.mage.database.model.observation.ObservationForm import mil.nga.sf.LineString import mil.nga.sf.Polygon @@ -49,39 +50,41 @@ class ShapeStyle: AnnotationStyle { private const val STROKE_WIDTH_ELEMENT = "strokeWidth" private const val MAX_ALPHA = 255.0f - fun fromObservation(observation: Observation, context: Context): AnnotationStyle { - var jsonStyle = observation.forms.firstOrNull()?.let { observationForm -> - val form = EventHelper.getInstance(context).getForm(observationForm.formId) - form.style?.let { formStyle -> - var style = JsonParser.parseString(formStyle)?.asJsonObjectOrNull() - if (style != null) { - if (form.primaryMapField != null) { - val primaryProperty = observationForm.properties.find { it.key == form.primaryMapField } - val primary = primaryProperty?.value?.toString() - - // Check for primary within the style object - style.get(primary)?.asJsonObjectOrNull()?.let { primaryStyle -> - style = primaryStyle - - if (form.secondaryMapField != null) { - val secondaryProperty = observationForm.properties.find { it.key == form.secondaryMapField } - val secondary = secondaryProperty?.value?.toString() - - // Check for secondary within the style type object - primaryStyle.get(secondary)?.asJsonObjectOrNull()?.let { secondaryStyle -> - style = secondaryStyle - } + fun fromObservation( + event: Event?, + formDefinition: Form?, + observationForm: ObservationForm?, + context: Context + ): AnnotationStyle { + var jsonStyle = formDefinition?.style?.let { formStyle -> + var style = JsonParser.parseString(formStyle)?.asJsonObjectOrNull() + if (style != null) { + if (formDefinition.primaryMapField != null) { + val primaryProperty = observationForm?.properties?.find { it.key == formDefinition.primaryMapField } + val primary = primaryProperty?.value?.toString() + + // Check for primary within the style object + style.get(primary)?.asJsonObjectOrNull()?.let { primaryStyle -> + style = primaryStyle + + if (formDefinition.secondaryMapField != null) { + val secondaryProperty = observationForm?.properties?.find { it.key == formDefinition.secondaryMapField } + val secondary = secondaryProperty?.value?.toString() + + // Check for secondary within the style type object + primaryStyle.get(secondary)?.asJsonObjectOrNull()?.let { secondaryStyle -> + style = secondaryStyle } } } } - - style } + + style } if (jsonStyle == null) { - jsonStyle = observation.event.style?.let { style -> + jsonStyle = event?.style?.let { style -> JsonParser.parseString(style)?.asJsonObjectOrNull() } } @@ -140,7 +143,11 @@ class ShapeStyle: AnnotationStyle { } } - fun fromForm(formState: FormState?, context: Context): ShapeStyle { + fun fromForm( + event: Event?, + formState: FormState?, + context: Context + ): ShapeStyle { var style = ShapeStyle(context) // Check for a style @@ -186,15 +193,14 @@ class ShapeStyle: AnnotationStyle { if (jsonStyle != null) { style = fromJson(jsonStyle, context) } else { - EventHelper.getInstance(context).read(formState.eventId)?.style?.let { jsonStyle -> - JsonParser.parseString(jsonStyle)?.asJsonObjectOrNull()?.let { jsonObject -> + event?.style?.let { + JsonParser.parseString(it)?.asJsonObjectOrNull()?.let { jsonObject -> style = fromJson(jsonObject, context) } } } } - return style } diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.java b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.java deleted file mode 100644 index 25b4eb070..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.java +++ /dev/null @@ -1,58 +0,0 @@ -package mil.nga.giat.mage.map.cache; - -import android.content.Context; -import android.os.Environment; - -import com.google.common.base.Predicate; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; - -import java.util.Collection; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.exceptions.LayerException; - -public class CacheOverlayFilter { - private Context context; - private Collection layers; - - private Predicate eventPredicate = new Predicate() { - @Override - public boolean apply(CacheOverlay overlay) { - if (overlay instanceof GeoPackageCacheOverlay) { - String filePath = ((GeoPackageCacheOverlay) overlay).getFilePath(); - if (filePath.startsWith(String.format("%s/MAGE/geopackages", context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)))) { - for (Layer layer : layers) { - String layerPath = String.format("geopackages/%s/%s", layer.getRemoteId(), layer.getFileName()); - if (filePath.endsWith(layerPath)) { - return true; - } - } - - return false; - } else { - return true; - } - } else { - return true; - } - } - }; - - public CacheOverlayFilter(Context context, Event event) { - this.context = context; - - try { - this.layers = LayerHelper.getInstance(context).readByEvent(event, "GeoPackage"); - } catch (LayerException e) { - e.printStackTrace(); - } - } - - public List filter(List overlays) { - return Lists.newArrayList(Iterables.filter(overlays, eventPredicate)); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.kt b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.kt new file mode 100644 index 000000000..a0c9cb66c --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheOverlayFilter.kt @@ -0,0 +1,33 @@ +package mil.nga.giat.mage.map.cache + +import android.content.Context +import android.os.Environment +import com.google.common.base.Predicate +import com.google.common.collect.Iterables +import com.google.common.collect.Lists +import mil.nga.giat.mage.database.model.layer.Layer + +class CacheOverlayFilter( + private val context: Context, + private val layers: List +) { + private val eventPredicate: Predicate = Predicate { overlay -> + if (overlay is GeoPackageCacheOverlay) { + val filePath = overlay.filePath + val downloadDirectory = "${context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)}/MAGE/geopackages" + if (filePath.startsWith(downloadDirectory)) { + layers.any { layer -> + filePath.endsWith("geopackages/${layer.remoteId}/${layer.fileName}") + } + } else { + true + } + } else { + true + } + } + + fun filter(overlays: List): List { + return Lists.newArrayList(Iterables.filter(overlays, eventPredicate)) + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.java b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.java deleted file mode 100644 index 7cba7d81e..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.java +++ /dev/null @@ -1,454 +0,0 @@ -package mil.nga.giat.mage.map.cache; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Environment; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.io.File; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import mil.nga.geopackage.GeoPackage; -import mil.nga.geopackage.GeoPackageFactory; -import mil.nga.geopackage.GeoPackageManager; -import mil.nga.geopackage.extension.nga.link.FeatureTileTableLinker; -import mil.nga.geopackage.features.index.FeatureIndexManager; -import mil.nga.geopackage.features.user.FeatureDao; -import mil.nga.geopackage.tiles.user.TileDao; -import mil.nga.geopackage.validate.GeoPackageValidate; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.cache.CacheUtils; -import mil.nga.giat.mage.cache.GeoPackageCacheUtils; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.exceptions.LayerException; -import mil.nga.giat.mage.sdk.utils.StorageUtility; -import mil.nga.sf.GeometryType; - -/** - * Created by wnewman on 2/11/16. - */ -public class CacheProvider { - - private static final String LOG_NAME = CacheProvider.class.getName(); - - private final Context context; - - private final Map cacheOverlays = Collections.synchronizedMap(new HashMap()); - private final List cacheOverlayListeners = Collections.synchronizedList(new ArrayList()); - - private static CacheProvider instance = null; - - private CacheProvider(Context context) { - this.context = context; - } - - public static synchronized CacheProvider getInstance(Context context) { - if (instance == null) { - instance = new CacheProvider(context); - } - - return instance; - } - - public interface OnCacheOverlayListener { - void onCacheOverlay(List cacheOverlays); - } - - public List getCacheOverlays() { - List copy = null; - synchronized(cacheOverlays) { - copy = Collections.unmodifiableList(new ArrayList<>(cacheOverlays.values())); - } - - return copy; - } - - public CacheOverlay getOverlay(String name){ - return cacheOverlays.get(name); - } - - public void registerCacheOverlayListener(OnCacheOverlayListener listener) { - registerCacheOverlayListener(listener, true); - } - - public void registerCacheOverlayListener(OnCacheOverlayListener listener, boolean fire) { - cacheOverlayListeners.add(listener); - if(fire) { - synchronized (cacheOverlays) { - listener.onCacheOverlay(getCacheOverlays()); - } - } - } - - public void addCacheOverlay(CacheOverlay cacheOverlay) { - cacheOverlays.put(cacheOverlay.getCacheName(), cacheOverlay); - } - - public boolean removeCacheOverlay(String name) { - return cacheOverlays.remove(name) != null; - } - - public void unregisterCacheOverlayListener(OnCacheOverlayListener listener) { - cacheOverlayListeners.remove(listener); - } - - public void refreshTileOverlays() { - enableAndRefreshTileOverlays(null); - } - - public void enableAndRefreshTileOverlays(String enableOverlayName) { - List overlayNames = null; - if(enableOverlayName != null) { - overlayNames = new ArrayList<>(1); - overlayNames.add(enableOverlayName); - } - TileOverlaysTask task = new TileOverlaysTask(overlayNames); - task.execute(); - } - - private void setCacheOverlays(List cacheOverlays) { - synchronized (this.cacheOverlays) { - this.cacheOverlays.clear(); - for(CacheOverlay overlay : cacheOverlays) { - addCacheOverlay(overlay); - } - } - - synchronized(cacheOverlayListeners) { - for (OnCacheOverlayListener listener : cacheOverlayListeners) { - listener.onCacheOverlay(cacheOverlays); - } - } - } - - private class TileOverlaysTask extends AsyncTask> { - - private final Set enable = new HashSet<>(); - - public TileOverlaysTask(Collection enable){ - if(enable != null) { - this.enable.addAll(enable); - } - } - - @Override - protected List doInBackground(Void... params) { - List overlays = new ArrayList<>(); - - // Add the existing external GeoPackage databases as cache overlays - GeoPackageManager geoPackageManager = GeoPackageFactory.getManager(context); - addGeoPackageCacheOverlays(context, overlays, geoPackageManager); - - // Get public external caches stored in /MapCache folder - Map storageLocations = StorageUtility.getReadableStorageLocations(); - for (File storageLocation : storageLocations.values()) { - File root = new File(storageLocation, context.getString(R.string.overlay_cache_directory)); - if (root.exists() && root.isDirectory() && root.canRead()) { - for (File cache : root.listFiles()) { - if(cache.canRead()) { - if (cache.isDirectory()) { - // found a cache - overlays.add(new XYZDirectoryCacheOverlay(cache.getName(), cache)); - } else if (GeoPackageValidate.hasGeoPackageExtension(cache)) { - GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, cache, geoPackageManager); - if (cacheOverlay != null) { - overlays.add(cacheOverlay); - } - } - } - } - } - } - - // Check internal/external application storage - File applicationCacheDirectory = CacheUtils.getApplicationCacheDirectory(context); - if (applicationCacheDirectory != null && applicationCacheDirectory.exists()) { - for (File cache : applicationCacheDirectory.listFiles()) { - if (GeoPackageValidate.hasGeoPackageExtension(cache)) { - GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, cache, geoPackageManager); - if (cacheOverlay != null) { - overlays.add(cacheOverlay); - } - } - } - } - - final Event event = EventHelper.getInstance(context).getCurrentEvent(); - - try { - List imageryLayers = LayerHelper.getInstance(context).readByEvent(event, "Imagery"); - - for(Layer imagery : imageryLayers){ - if(imagery.getFormat() == null || !imagery.getFormat().equalsIgnoreCase("wms")) { - overlays.add(new URLCacheOverlay(imagery.getName(), new URL(imagery.getUrl()), imagery)); - }else{ - overlays.add(new WMSCacheOverlay(imagery.getName(), new URL(imagery.getUrl()), imagery)); - } - } - }catch(Exception e){ - Log.w(LOG_NAME, "Failed to load imagery layers", e); - } - - try { - List featureLayers = LayerHelper.getInstance(context).readByEvent(event,"Feature"); - - for(Layer feature : featureLayers){ - if(feature.isLoaded()){ - overlays.add(new StaticFeatureCacheOverlay(feature.getName(), feature.getId())); - } - } - }catch(Exception e){ - Log.w(LOG_NAME, "Failed to load imagery layers", e); - } - - // Set what should be enabled based on preferences. - boolean update = false; - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - Set updatedEnabledOverlays = new HashSet<>(); - updatedEnabledOverlays.addAll(preferences.getStringSet(context.getString(R.string.tileOverlaysKey), Collections.emptySet())); - updatedEnabledOverlays.addAll(preferences.getStringSet(context.getString(R.string.onlineLayersKey), Collections.emptySet())); - Set enabledOverlays = new HashSet<>(); - enabledOverlays.addAll(updatedEnabledOverlays); - - // Determine which caches are enabled - for (CacheOverlay cacheOverlay : overlays) { - - // Check and enable the cache - String cacheName = cacheOverlay.getCacheName(); - if (enabledOverlays.remove(cacheName)) { - cacheOverlay.setEnabled(true); - } - - // Check the child caches - for (CacheOverlay childCache : cacheOverlay.getChildren()) { - if (enabledOverlays.remove(childCache.getCacheName())) { - childCache.setEnabled(true); - cacheOverlay.setEnabled(true); - } - } - - // Check for new caches to enable in the overlays and preferences - if (enable.contains(cacheName)) { - - update = true; - cacheOverlay.setEnabled(true); - cacheOverlay.setAdded(true); - if (cacheOverlay.isSupportsChildren()) { - for (CacheOverlay childCache : cacheOverlay.getChildren()) { - childCache.setEnabled(true); - updatedEnabledOverlays.add(childCache.getCacheName()); - } - } else { - updatedEnabledOverlays.add(cacheName); - } - } - - } - - // Remove overlays in the preferences that no longer exist - if (!enabledOverlays.isEmpty()) { - updatedEnabledOverlays.removeAll(enabledOverlays); - update = true; - } - - // If new enabled cache overlays, update them in the preferences - if (update) { - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet(context.getString(R.string.tileOverlaysKey), updatedEnabledOverlays); - editor.apply(); - } - - return overlays; - } - - @Override - protected void onPostExecute(List result) { - setCacheOverlays(result); - } - - /** - * Add GeoPackage Cache Overlay for the existing databases - * - * @param context - * @param overlays - * @param geoPackageManager - */ - private void addGeoPackageCacheOverlays(Context context, List overlays, GeoPackageManager geoPackageManager) { - - // Delete any GeoPackages where the file is no longer accessible - geoPackageManager.deleteAllMissingExternal(); - - final Map nonSideloadedGeopackages = new HashMap<>(); - try { - LayerHelper layerHelper = LayerHelper.getInstance(context); - List layers = layerHelper.readAll("GeoPackage"); - for (Layer layer : layers) { - if (!layer.isLoaded()) { - continue; - } - - String relativePath = layer.getRelativePath(); - if (relativePath != null) { - File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), relativePath); - nonSideloadedGeopackages.put(file.getName(), file.getName()); - if (!file.exists()) { - layer.setLoaded(true); - layerHelper.update(layer); - } - } - } - } catch (LayerException e) { - Log.i(LOG_NAME, "Error reconciling downloaded layers", e); - } - - // Add each existing database as a cache - List externalDatabases = geoPackageManager.externalDatabases(); - for (String database : externalDatabases) { - GeoPackageCacheOverlay cacheOverlay = getGeoPackageCacheOverlay(context, geoPackageManager, database); - if (cacheOverlay != null) { - File f = new File(cacheOverlay.getFilePath()); - //TODO what happens if there are 2 geopackages with the same name - if(!nonSideloadedGeopackages.containsKey(f.getName())){ - cacheOverlay.setSideloaded(true); - } - overlays.add(cacheOverlay); - } - } - } - } - - - - /** - * Get GeoPackage Cache Overlay for the database file - * - * @param context - * @param cache - * @param geoPackageManager - * @return cache overlay - */ - public GeoPackageCacheOverlay getGeoPackageCacheOverlay(Context context, File cache, GeoPackageManager geoPackageManager) { - - GeoPackageCacheOverlay cacheOverlay = null; - - // Import the GeoPackage if needed - String cacheName = GeoPackageCacheUtils.importGeoPackage(geoPackageManager, cache); - if(cacheName != null){ - // Get the GeoPackage overlay - cacheOverlay = getGeoPackageCacheOverlay(context, geoPackageManager, cacheName); - } - - return cacheOverlay; - } - - /** - * Get the GeoPackage database as a cache overlay - * - * @param context - * @param geoPackageManager - * @param database - * @return cache overlay - */ - private GeoPackageCacheOverlay getGeoPackageCacheOverlay(Context context, GeoPackageManager geoPackageManager, String database) { - - GeoPackageCacheOverlay cacheOverlay = null; - GeoPackage geoPackage = null; - - // Add the GeoPackage overlay - try { - geoPackage = geoPackageManager.open(database); - - List tables = new ArrayList<>(); - - // GeoPackage tile tables, build a mapping between table name and the created cache overlays - Map tileCacheOverlays = new HashMap<>(); - List tileTables = geoPackage.getTileTables(); - for (String tileTable : tileTables) { - String tableCacheName = CacheOverlay.buildChildCacheName(database, tileTable); - TileDao tileDao = geoPackage.getTileDao(tileTable); - int count = tileDao.count(); - int minZoom = (int) tileDao.getMinZoom(); - int maxZoom = (int) tileDao.getMaxZoom(); - GeoPackageTileTableCacheOverlay tableCache = new GeoPackageTileTableCacheOverlay(tileTable, database, tableCacheName, count, minZoom, maxZoom); - tileCacheOverlays.put(tileTable, tableCache); - } - - // Get a linker to find tile tables linked to features - FeatureTileTableLinker linker = new FeatureTileTableLinker(geoPackage); - Map linkedTileCacheOverlays = new HashMap<>(); - - // GeoPackage feature tables - List featureTables = geoPackage.getFeatureTables(); - for (String featureTable : featureTables) { - String tableCacheName = CacheOverlay.buildChildCacheName(database, featureTable); - FeatureDao featureDao = geoPackage.getFeatureDao(featureTable); - int count = featureDao.count(); - GeometryType geometryType = featureDao.getGeometryType(); - FeatureIndexManager indexer = new FeatureIndexManager(context, geoPackage, featureDao); - boolean indexed = indexer.isIndexed(); - indexer.close(); - int minZoom = 0; - if (indexed) { - minZoom = featureDao.getZoomLevel() + context.getResources().getInteger(R.integer.geopackage_feature_tiles_min_zoom_offset); - minZoom = Math.max(minZoom, 0); - minZoom = Math.min(minZoom, GeoPackageFeatureTableCacheOverlay.MAX_ZOOM); - } - GeoPackageFeatureTableCacheOverlay tableCache = new GeoPackageFeatureTableCacheOverlay(featureTable, database, tableCacheName, count, minZoom, indexed, geometryType); - - // If indexed, check for linked tile tables - if(indexed){ - List linkedTileTables = linker.getTileTablesForFeatureTable(featureTable); - for(String linkedTileTable: linkedTileTables){ - // Get the tile table cache overlay - GeoPackageTileTableCacheOverlay tileCacheOverlay = tileCacheOverlays.get(linkedTileTable); - if(tileCacheOverlay != null){ - // Remove from tile cache overlays so the tile table is not added as stand alone, and add to the linked overlays - tileCacheOverlays.remove(linkedTileTable); - linkedTileCacheOverlays.put(linkedTileTable, tileCacheOverlay); - }else{ - // Another feature table may already be linked to this table, so check the linked overlays - tileCacheOverlay = linkedTileCacheOverlays.get(linkedTileTable); - } - - // Add the linked tile table to the feature table - if(tileCacheOverlay != null){ - tableCache.addLinkedTileTable(tileCacheOverlay); - } - } - } - - tables.add(tableCache); - } - - // Add stand alone tile tables that were not linked to feature tables - for(GeoPackageTileTableCacheOverlay tileCacheOverlay: tileCacheOverlays.values()){ - tables.add(tileCacheOverlay); - } - - // Create the GeoPackage overlay with child tables - cacheOverlay = new GeoPackageCacheOverlay(database, geoPackage.getPath(), tables); - } catch (Exception e) { - Log.e(LOG_NAME, "Could not get geopackage cache", e); - } finally { - if (geoPackage != null) { - geoPackage.close(); - } - } - - return cacheOverlay; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.kt b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.kt new file mode 100644 index 000000000..ecd1a4ec9 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/cache/CacheProvider.kt @@ -0,0 +1,416 @@ +package mil.nga.giat.mage.map.cache + +import android.app.Application +import android.content.SharedPreferences +import android.os.Environment +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mil.nga.geopackage.GeoPackage +import mil.nga.geopackage.GeoPackageFactory +import mil.nga.geopackage.GeoPackageManager +import mil.nga.geopackage.extension.nga.link.FeatureTileTableLinker +import mil.nga.geopackage.features.index.FeatureIndexManager +import mil.nga.geopackage.validate.GeoPackageValidate +import mil.nga.giat.mage.R +import mil.nga.giat.mage.cache.CacheUtils +import mil.nga.giat.mage.cache.GeoPackageCacheUtils +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.sdk.exceptions.LayerException +import mil.nga.giat.mage.sdk.utils.StorageUtility +import java.io.File +import java.net.URL +import java.util.Collections +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CacheProvider @Inject constructor( + private val application: Application, + private val layerLocalDataSource: LayerLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val preferences: SharedPreferences +) { + val cacheOverlays = Collections.synchronizedMap(HashMap()) + private val cacheOverlayListeners = Collections.synchronizedList(ArrayList()) + + interface OnCacheOverlayListener { + fun onCacheOverlay(cacheOverlays: List) + } + + private fun getCacheOverlays(): List { + var copy: List + synchronized(cacheOverlays) { + copy = cacheOverlays.values.toList() + } + return copy + } + + fun getOverlay(name: String): CacheOverlay? { + return cacheOverlays[name] + } + + fun registerCacheOverlayListener(listener: OnCacheOverlayListener, fire: Boolean = true) { + cacheOverlayListeners.add(listener) + if (fire) { + synchronized(cacheOverlays) { listener.onCacheOverlay(getCacheOverlays()) } + } + } + + fun addCacheOverlay(cacheOverlay: CacheOverlay?) { + cacheOverlays[cacheOverlay!!.cacheName] = cacheOverlay + } + + fun removeCacheOverlay(name: String): Boolean { + return cacheOverlays.remove(name) != null + } + + fun unregisterCacheOverlayListener(listener: OnCacheOverlayListener) { + cacheOverlayListeners.remove(listener) + } + + suspend fun refreshTileOverlays() = withContext(Dispatchers.IO) { + enableAndRefreshTileOverlays() + } + + suspend fun enableAndRefreshTileOverlays(enableOverlayName: String? = null) { + val overlayNames = mutableListOf().apply { + enableOverlayName?.let { add(it) } + } + + enableAndRefreshTileOverlays(overlayNames) + } + + private fun setCacheOverlays(cacheOverlays: List) { + synchronized(this.cacheOverlays) { + this.cacheOverlays.clear() + for (overlay in cacheOverlays) { + addCacheOverlay(overlay) + } + } + synchronized(cacheOverlayListeners) { + for (listener in cacheOverlayListeners) { + listener.onCacheOverlay(cacheOverlays) + } + } + } + + suspend private fun enableAndRefreshTileOverlays(enable: List): List { + val event = eventLocalDataSource.currentEvent ?: return emptyList() + + val overlays = mutableListOf() + + // Add the existing external GeoPackage databases as cache overlays + val geoPackageManager = GeoPackageFactory.getManager(application) + overlays.addAll(getGeoPackageCacheOverlays(geoPackageManager)) + + // Get public external caches stored in /MapCache folder + val storageLocations = StorageUtility.getReadableStorageLocations() + for (storageLocation in storageLocations.values) { + val root = File(storageLocation, application.getString(R.string.overlay_cache_directory)) + if (root.exists() && root.isDirectory && root.canRead()) { + root.listFiles()?.forEach { cache -> + if (cache.canRead()) { + if (cache.isDirectory) { + overlays.add(XYZDirectoryCacheOverlay(cache.name, cache)) + } else if (GeoPackageValidate.hasGeoPackageExtension(cache)) { + getGeoPackageCacheOverlay(cache, geoPackageManager)?.let { + overlays.add(it) + } + } + } + } + } + } + + // Check internal/external application storage + val applicationCacheDirectory = CacheUtils.getApplicationCacheDirectory(application) + if (applicationCacheDirectory != null && applicationCacheDirectory.exists()) { + applicationCacheDirectory.listFiles()?.forEach { cache -> + if (GeoPackageValidate.hasGeoPackageExtension(cache)) { + getGeoPackageCacheOverlay(cache, geoPackageManager)?.let { + overlays.add(it) + } + } + } + } + + for (imagery in layerLocalDataSource.readByEvent(event, "Imagery")) { + if (imagery.format == null || !imagery.format.equals("wms", ignoreCase = true)) { + overlays.add(URLCacheOverlay(imagery.name, URL(imagery.url), imagery)) + } else { + overlays.add(WMSCacheOverlay(imagery.name, URL(imagery.url), imagery)) + } + } + + for (feature in layerLocalDataSource.readByEvent(event, "Feature")) { + if (feature.isLoaded) { + overlays.add(StaticFeatureCacheOverlay(feature.name, feature.id)) + } + } + + // Set what should be enabled based on preferences. + var update = false + + val updatedEnabledOverlays: MutableSet = HashSet() + updatedEnabledOverlays.addAll( + preferences.getStringSet( + application.getString(R.string.tileOverlaysKey), + emptySet() + )!! + ) + updatedEnabledOverlays.addAll( + preferences.getStringSet( + application.getString(R.string.onlineLayersKey), + emptySet() + )!! + ) + val enabledOverlays: MutableSet = HashSet() + enabledOverlays.addAll(updatedEnabledOverlays) + + // Determine which caches are enabled + for (cacheOverlay in overlays) { + // Check and enable the cache + val cacheName = cacheOverlay.cacheName + if (enabledOverlays.remove(cacheName)) { + cacheOverlay.isEnabled = true + } + + // Check the child caches + for (childCache in cacheOverlay.children) { + if (enabledOverlays.remove(childCache.cacheName)) { + childCache.isEnabled = true + cacheOverlay.isEnabled = true + } + } + + // Check for new caches to enable in the overlays and preferences + if (enable.contains(cacheName)) { + update = true + cacheOverlay.isEnabled = true + cacheOverlay.isAdded = true + if (cacheOverlay.isSupportsChildren) { + for (childCache in cacheOverlay.children) { + childCache.isEnabled = true + updatedEnabledOverlays.add(childCache.cacheName) + } + } else { + updatedEnabledOverlays.add(cacheName) + } + } + } + + // Remove overlays in the preferences that no longer exist + if (enabledOverlays.isNotEmpty()) { + updatedEnabledOverlays.removeAll(enabledOverlays) + update = true + } + + // If new enabled cache overlays, update them in the preferences + if (update) { + val editor = preferences.edit() + editor.putStringSet(application.getString(R.string.tileOverlaysKey), updatedEnabledOverlays) + editor.apply() + } + + CoroutineScope(Dispatchers.Main).launch { + setCacheOverlays(overlays) + } + + return overlays + } + + /** + * Add GeoPackage Cache Overlay for the existing databases + * + * @param context + * @param overlays + * @param geoPackageManager + */ + suspend private fun getGeoPackageCacheOverlays( + geoPackageManager: GeoPackageManager + ): List { + val overlays = mutableListOf() + + // Delete any GeoPackages where the file is no longer accessible + geoPackageManager.deleteAllMissingExternal() + val remoteGeopackages = mutableMapOf() + try { + val layers = layerLocalDataSource.readAll("GeoPackage") + for (layer in layers) { + if (!layer.isLoaded) { + continue + } + val relativePath = layer.relativePath + if (relativePath != null) { + val file = File( + application.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + relativePath + ) + remoteGeopackages[file.name] = file.name + if (!file.exists()) { + layer.isLoaded = true + layerLocalDataSource.update(layer) + } + } + } + } catch (e: LayerException) { + Log.i(LOG_NAME, "Error reconciling downloaded layers", e) + } + + // Add each existing database as a cache + val externalDatabases = geoPackageManager.externalDatabases() + for (database in externalDatabases) { + val cacheOverlay = getGeoPackageCacheOverlay(geoPackageManager, database) + if (cacheOverlay != null) { + val f = File(cacheOverlay.filePath) + //TODO what happens if there are 2 geopackages with the same name + if (!remoteGeopackages.containsKey(f.name)) { + cacheOverlay.isSideloaded = true + } + overlays.add(cacheOverlay) + } + } + + return overlays + } + + /** + * Get GeoPackage Cache Overlay for the database file + * + * @param cache + * @param geoPackageManager + * @return cache overlay + */ + fun getGeoPackageCacheOverlay( + cache: File?, + geoPackageManager: GeoPackageManager + ): GeoPackageCacheOverlay? { + var cacheOverlay: GeoPackageCacheOverlay? = null + + // Import the GeoPackage if needed + val cacheName = GeoPackageCacheUtils.importGeoPackage(geoPackageManager, cache) + if (cacheName != null) { + // Get the GeoPackage overlay + cacheOverlay = getGeoPackageCacheOverlay(geoPackageManager, cacheName) + } + return cacheOverlay + } + + /** + * Get the GeoPackage database as a cache overlay + * + * @param geoPackageManager + * @param database + * @return cache overlay + */ + private fun getGeoPackageCacheOverlay( + geoPackageManager: GeoPackageManager, + database: String + ): GeoPackageCacheOverlay? { + var cacheOverlay: GeoPackageCacheOverlay? = null + var geoPackage: GeoPackage? = null + + // Add the GeoPackage overlay + try { + geoPackage = geoPackageManager.open(database) + val tables: MutableList = ArrayList() + + // GeoPackage tile tables, build a mapping between table name and the created cache overlays + val tileCacheOverlays: MutableMap = HashMap() + val tileTables = geoPackage.tileTables + for (tileTable in tileTables) { + val tableCacheName = CacheOverlay.buildChildCacheName(database, tileTable) + val tileDao = geoPackage.getTileDao(tileTable) + val count = tileDao.count() + val minZoom = tileDao.minZoom.toInt() + val maxZoom = tileDao.maxZoom.toInt() + val tableCache = GeoPackageTileTableCacheOverlay( + tileTable, + database, + tableCacheName, + count, + minZoom, + maxZoom + ) + tileCacheOverlays[tileTable] = tableCache + } + + // Get a linker to find tile tables linked to features + val linker = FeatureTileTableLinker(geoPackage) + val linkedTileCacheOverlays: MutableMap = + HashMap() + + // GeoPackage feature tables + val featureTables = geoPackage.featureTables + for (featureTable in featureTables) { + val tableCacheName = CacheOverlay.buildChildCacheName(database, featureTable) + val featureDao = geoPackage.getFeatureDao(featureTable) + val count = featureDao.count() + val geometryType = featureDao.geometryType + val indexer = FeatureIndexManager(application, geoPackage, featureDao) + val indexed = indexer.isIndexed + indexer.close() + var minZoom = 0 + if (indexed) { + minZoom = + featureDao.zoomLevel + application.resources.getInteger(R.integer.geopackage_feature_tiles_min_zoom_offset) + minZoom = minZoom.coerceAtLeast(0) + minZoom = minZoom.coerceAtMost(GeoPackageFeatureTableCacheOverlay.MAX_ZOOM) + } + val tableCache = GeoPackageFeatureTableCacheOverlay( + featureTable, + database, + tableCacheName, + count, + minZoom, + indexed, + geometryType + ) + + // If indexed, check for linked tile tables + if (indexed) { + val linkedTileTables = linker.getTileTablesForFeatureTable(featureTable) + for (linkedTileTable in linkedTileTables) { + // Get the tile table cache overlay + var tileCacheOverlay = tileCacheOverlays[linkedTileTable] + if (tileCacheOverlay != null) { + // Remove from tile cache overlays so the tile table is not added as stand alone, and add to the linked overlays + tileCacheOverlays.remove(linkedTileTable) + linkedTileCacheOverlays[linkedTileTable] = tileCacheOverlay + } else { + // Another feature table may already be linked to this table, so check the linked overlays + tileCacheOverlay = linkedTileCacheOverlays[linkedTileTable] + } + + // Add the linked tile table to the feature table + if (tileCacheOverlay != null) { + tableCache.addLinkedTileTable(tileCacheOverlay) + } + } + } + tables.add(tableCache) + } + + // Add stand alone tile tables that were not linked to feature tables + for (tileCacheOverlay in tileCacheOverlays.values) { + tables.add(tileCacheOverlay) + } + + // Create the GeoPackage overlay with child tables + cacheOverlay = GeoPackageCacheOverlay(database, geoPackage.path, tables) + } catch (e: Exception) { + Log.e(LOG_NAME, "Could not get geopackage cache", e) + } finally { + geoPackage?.close() + } + return cacheOverlay + } + + companion object { + private val LOG_NAME = CacheProvider::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/URLCacheOverlay.java b/mage/src/main/java/mil/nga/giat/mage/map/cache/URLCacheOverlay.java index 0f1792b9c..1e5eff17f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/cache/URLCacheOverlay.java +++ b/mage/src/main/java/mil/nga/giat/mage/map/cache/URLCacheOverlay.java @@ -4,7 +4,7 @@ import java.net.URL; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; +import mil.nga.giat.mage.database.model.layer.Layer; public class URLCacheOverlay extends CacheOverlay { diff --git a/mage/src/main/java/mil/nga/giat/mage/map/cache/WMSCacheOverlay.java b/mage/src/main/java/mil/nga/giat/mage/map/cache/WMSCacheOverlay.java index 156dbf8a8..5c9e66a98 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/cache/WMSCacheOverlay.java +++ b/mage/src/main/java/mil/nga/giat/mage/map/cache/WMSCacheOverlay.java @@ -2,7 +2,7 @@ import java.net.URL; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; +import mil.nga.giat.mage.database.model.layer.Layer; public class WMSCacheOverlay extends URLCacheOverlay{ diff --git a/mage/src/main/java/mil/nga/giat/mage/map/detail/UserPhone.kt b/mage/src/main/java/mil/nga/giat/mage/map/detail/UserPhone.kt index fa1b76307..4187dd752 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/detail/UserPhone.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/detail/UserPhone.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.LiveData -import mil.nga.giat.mage.sdk.datastore.user.User +import mil.nga.giat.mage.database.model.user.User import mil.nga.giat.mage.ui.theme.MageTheme sealed class UserPhoneAction { diff --git a/mage/src/main/java/mil/nga/giat/mage/map/download/GeoPackageDownloadManager.java b/mage/src/main/java/mil/nga/giat/mage/map/download/GeoPackageDownloadManager.java index 1579d3724..bcc409762 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/download/GeoPackageDownloadManager.java +++ b/mage/src/main/java/mil/nga/giat/mage/map/download/GeoPackageDownloadManager.java @@ -21,17 +21,19 @@ import java.util.Collection; import java.util.List; +import javax.inject.Singleton; + import mil.nga.geopackage.GeoPackageFactory; import mil.nga.geopackage.GeoPackageManager; import mil.nga.giat.mage.R; import mil.nga.giat.mage.map.cache.CacheOverlay; import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; +import mil.nga.giat.mage.database.model.layer.Layer; +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource; +import mil.nga.giat.mage.database.model.event.Event; import mil.nga.giat.mage.sdk.exceptions.LayerException; +@Singleton public class GeoPackageDownloadManager { public interface GeoPackageLoadListener { @@ -46,18 +48,25 @@ public interface GeoPackageDownloadListener { private final Context context; private final String baseUrl; - private final LayerHelper layerHelper; + private final CacheProvider cacheProvider; + private final LayerLocalDataSource layerLocalDataSource; private final GeoPackageDownloadListener listener; private final DownloadManager downloadManager; private final BroadcastReceiver downloadReceiver = new GeoPackageDownloadReceiver(); private final Object downloadLock = new Object(); - public GeoPackageDownloadManager(Context context, GeoPackageDownloadListener listener) { + public GeoPackageDownloadManager( + Context context, + CacheProvider cacheProvider, + LayerLocalDataSource layerLocalDataSource, + GeoPackageDownloadListener listener + ) { this.context = context; baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); + this.cacheProvider = cacheProvider; + this.layerLocalDataSource = layerLocalDataSource; this.listener = listener; - layerHelper = LayerHelper.getInstance(context); downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); } @@ -69,9 +78,8 @@ public void onPause() { context.unregisterReceiver(downloadReceiver); } - public void downloadGeoPackage(Layer layer) { + public void downloadGeoPackage(Event event, Layer layer) { String token = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.tokenKey), null); - final Event event = EventHelper.getInstance(context).getCurrentEvent(); DownloadManager.Request request = new DownloadManager.Request(Uri.parse(String.format("%s/api/events/%s/layers/%s", baseUrl, event.getRemoteId(), layer.getRemoteId()))); request.setTitle("MAGE GeoPackage Download"); @@ -86,7 +94,7 @@ public void downloadGeoPackage(Layer layer) { try { layer.setDownloadId(downloadId); - layerHelper.update(layer); + layerLocalDataSource.update(layer); } catch (LayerException e) { Log.e(LOG_NAME, "Error saving layer download id", e); } @@ -120,7 +128,7 @@ public String isFailed(Layer layer) { status = getDownloadFailureStatus(cursor); try { layer.setDownloadId(null); - layerHelper.update(layer); + layerLocalDataSource.update(layer); } catch (LayerException e) { Log.e(LOG_NAME, "Error saving layer download id", e); } @@ -132,12 +140,7 @@ public String isFailed(Layer layer) { } public void reconcileDownloads(Collection layers, GeoPackageLoadListener listener) { - Predicate notDownloadedPredicate = new Predicate() { - @Override - public boolean apply(Layer layer) { - return !layer.isLoaded(); - } - }; + Predicate notDownloadedPredicate = (Predicate) layer -> !layer.isLoaded(); List notDownloaded = Lists.newArrayList(Iterables.filter(layers, notDownloadedPredicate)); new GeoPackageLoaderTask(listener).execute(notDownloaded.toArray(new Layer[notDownloaded.size()])); @@ -171,21 +174,20 @@ public String getRelativePath(Layer layer) { private void loadGeopackage(long downloadId, GeoPackageDownloadListener listener) { synchronized (downloadLock) { try { - Layer layer = layerHelper.getByDownloadId(downloadId); + Layer layer = layerLocalDataSource.getByDownloadId(downloadId); if (layer != null) { String relativePath = getRelativePath(layer); GeoPackageManager geoPackageManager = GeoPackageFactory.getManager(context); - CacheProvider cacheProvider = CacheProvider.getInstance(context); File cache = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), relativePath); - CacheOverlay overlay = cacheProvider.getGeoPackageCacheOverlay(context, cache, geoPackageManager); + CacheOverlay overlay = cacheProvider.getGeoPackageCacheOverlay(cache, geoPackageManager); if (overlay != null) { cacheProvider.addCacheOverlay(overlay); } layer.setRelativePath(relativePath); layer.setLoaded(true); - layerHelper.update(layer); + layerLocalDataSource.update(layer); if (listener != null) { listener.onGeoPackageDownloaded(layer, overlay); diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/LayerNameComparator.java b/mage/src/main/java/mil/nga/giat/mage/map/preference/LayerNameComparator.java index 4530d7887..bf281e647 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/LayerNameComparator.java +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/LayerNameComparator.java @@ -5,7 +5,7 @@ import java.util.Comparator; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; +import mil.nga.giat.mage.database.model.layer.Layer; public class LayerNameComparator implements Comparator { @Override diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapLayerPreferences.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapLayerPreferences.kt index 5a2787a42..600557d9b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapLayerPreferences.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapLayerPreferences.kt @@ -3,34 +3,37 @@ package mil.nga.giat.mage.map.preference import android.app.Activity import android.content.SharedPreferences import androidx.preference.SwitchPreferenceCompat -import mil.nga.giat.mage.data.feed.Feed +import mil.nga.giat.mage.database.model.feed.Feed import javax.inject.Inject class MapLayerPreferences @Inject constructor( - val preferences: SharedPreferences + val preferences: SharedPreferences ) { - companion object { - private const val ENABLED_FEED_LAYERS_KEY = "enabled_feed_layers_%s" - } + companion object { + private const val ENABLED_FEED_LAYERS_KEY = "enabled_feed_layers_%s" + } - fun getEnabledFeeds(eventId: Long): Set { - return preferences.getStringSet(ENABLED_FEED_LAYERS_KEY.format(eventId), HashSet())!! - } + fun getEnabledFeeds(eventId: Long?): Set { + return eventId?.let { + preferences.getStringSet(ENABLED_FEED_LAYERS_KEY.format(it), null) ?: emptySet() + } ?: emptySet() + } - fun setEnabledFeeds(eventId: Long, feedIds: Set) { - preferences.edit() - .putStringSet(ENABLED_FEED_LAYERS_KEY.format((eventId)), feedIds) - .apply() - } + fun setEnabledFeeds(eventId: Long?, feedIds: Set) { + eventId?.let { + preferences.edit() + .putStringSet(ENABLED_FEED_LAYERS_KEY.format(it), feedIds) + .apply() + } + } - fun mapFeedPreference(feed: Feed, activity: Activity, checked: Boolean): SwitchPreferenceCompat { - val preference = SwitchPreferenceCompat(activity) + fun mapFeedPreference(feed: Feed, activity: Activity, checked: Boolean): SwitchPreferenceCompat { + val preference = SwitchPreferenceCompat(activity) - preference.title = feed.title - preference.summary = feed.summary - preference.isChecked = checked + preference.title = feed.title + preference.summary = feed.summary + preference.isChecked = checked - return preference - } - + return preference + } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.java b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.java deleted file mode 100644 index 6d76ad5ff..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.java +++ /dev/null @@ -1,162 +0,0 @@ -package mil.nga.giat.mage.map.preference; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.lifecycle.ViewModelProvider; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceScreen; -import androidx.preference.SwitchPreferenceCompat; - -import com.google.common.collect.Collections2; - -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.data.feed.Feed; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; - -/** - * Provides map configuration driven settings that are available to the user. - * Check mappreferences.xml for the configuration. - * - * @author newmanw - */ -@AndroidEntryPoint -public class MapPreferencesActivity extends AppCompatActivity { - - public static String LOG_NAME = MapPreferencesActivity.class.getName(); - - public static final int TILE_OVERLAY_ACTIVITY = 100; - public static final int ONLINE_LAYERS_OVERLAY_ACTIVITY = 200; - - private MapPreferenceFragment preference = new MapPreferenceFragment(); - - @AndroidEntryPoint - public static class MapPreferenceFragment extends PreferenceFragmentCompat { - private MapPreferencesViewModel viewModel; - - @Inject - protected SharedPreferences preferences; - - @Inject - protected MapLayerPreferences mapLayerPreferences; - - private Event event; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - viewModel = new ViewModelProvider(this).get(MapPreferencesViewModel.class); - viewModel.getFeeds().observe(this, this::onFeeds); - } - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - addPreferencesFromResource(R.xml.mappreferences); - } - - @Override - public void onResume() { - super.onResume(); - - event = EventHelper.getInstance(getActivity().getApplicationContext()).getCurrentEvent(); - viewModel.setEvent(event.getRemoteId()); - - findPreference(getString(R.string.tileOverlaysKey)).setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(getActivity(), TileOverlayPreferenceActivity.class); - getActivity().startActivityForResult(intent, TILE_OVERLAY_ACTIVITY); - return true; - }); - - findPreference(getString(R.string.onlineLayersKey)).setOnPreferenceClickListener(preference -> { - Intent intent = new Intent(getActivity(), OnlineLayersPreferenceActivity.class); - getActivity().startActivityForResult(intent, ONLINE_LAYERS_OVERLAY_ACTIVITY); - return true; - }); - - SwitchPreferenceCompat showMGRS = findPreference(getString(R.string.showMGRSKey)); - SwitchPreferenceCompat showGARS = findPreference(getString(R.string.showGARSKey)); - - showMGRS.setOnPreferenceChangeListener((preference, newValue) -> { - if ((boolean) newValue) { - showGARS.setChecked(false); - } - return true; - }); - showGARS.setOnPreferenceChangeListener((preference, newValue) -> { - if ((boolean) newValue) { - showMGRS.setChecked(false); - } - return true; - }); - - // TODO : Remove the below and rework OverlayPreference to have a 'entities' similar to a list preference, these would be the 'display values' - try { - List layers = LayerHelper.getInstance(getContext()).readByEvent(event, "GeoPackage"); - layers.addAll(LayerHelper.getInstance(getContext()).readByEvent(event, "Feature")); - layers.addAll(LayerHelper.getInstance(getContext()).readByEvent(event, "Imagery")); - Collection available = Collections2.filter(layers, layer -> !layer.isLoaded()); - - OverlayPreference p = (OverlayPreference) findPreference(getResources().getString(R.string.tileOverlaysKey)); - p.setAvailableDownloads(!available.isEmpty()); - } catch (Exception e) { - Log.e(LOG_NAME, "Problem setting preference.", e); - } - } - - @Override - public void onPause() { - super.onPause(); - - findPreference(getString(R.string.tileOverlaysKey)).setOnPreferenceClickListener(null); - findPreference(getString(R.string.onlineLayersKey)).setOnPreferenceClickListener(null); - } - - private void onFeeds(List feeds) { - PreferenceScreen screen = getPreferenceScreen(); - Set enabledFeeds = mapLayerPreferences.getEnabledFeeds(event.getId()); - - for (Feed feed : feeds) { - PreferenceCategory category = (PreferenceCategory) screen.getPreference(screen.getPreferenceCount() - 1); - SwitchPreferenceCompat feedPreference = mapLayerPreferences.mapFeedPreference(feed, getActivity(), enabledFeeds.contains(feed.getId())); - feedPreference.setOnPreferenceClickListener( preference -> { - onFeedClick(feed, feedPreference.isChecked()); - return true; - }); - category.addPreference(feedPreference); - } - } - - private void onFeedClick(Feed feed, boolean on) { - Set feeds = mapLayerPreferences.getEnabledFeeds(event.getId()); - if (on) { - feeds.add(feed.getId()); - } else { - feeds.remove(feed.getId()); - } - - mapLayerPreferences.setEnabledFeeds(event.getId(), feeds); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getSupportFragmentManager().beginTransaction().replace(android.R.id.content, preference).commit(); - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.kt new file mode 100644 index 000000000..479f8b983 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesActivity.kt @@ -0,0 +1,139 @@ +package mil.nga.giat.mage.map.preference + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import dagger.hilt.android.AndroidEntryPoint +import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.layer.Layer +import javax.inject.Inject + +@AndroidEntryPoint +class MapPreferencesActivity : AppCompatActivity() { + private val preference = MapPreferenceFragment() + + @AndroidEntryPoint + class MapPreferenceFragment : PreferenceFragmentCompat() { + + @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var mapLayerPreferences: MapLayerPreferences + @Inject lateinit var layerLocalDataSource: LayerLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + + private var event: Event? = null + private lateinit var viewModel: MapPreferencesViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = ViewModelProvider(this).get(MapPreferencesViewModel::class.java) + viewModel.feeds.observe(this) { feeds: List -> onFeeds(feeds) } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.mappreferences) + } + + override fun onResume() { + super.onResume() + event = eventLocalDataSource.currentEvent + viewModel.setEvent(event?.remoteId) + + findPreference(getString(R.string.tileOverlaysKey))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(activity, TileOverlayPreferenceActivity::class.java) + Log.i("Billy", "MapPrefs on tile overlay click") + requireActivity().startActivityForResult(intent, TILE_OVERLAY_ACTIVITY) + true + } + + findPreference(getString(R.string.onlineLayersKey))?.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(activity, OnlineLayersPreferenceActivity::class.java) + requireActivity().startActivityForResult(intent, ONLINE_LAYERS_OVERLAY_ACTIVITY) + true + } + val showMGRS = findPreference(getString(R.string.showMGRSKey)) + val showGARS = findPreference(getString(R.string.showGARSKey)) + showMGRS?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + if (newValue as Boolean) { + showGARS?.isChecked = false + } + true + } + showGARS?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + if (newValue as Boolean) { + showMGRS?.isChecked = false + } + true + } + + // TODO : Remove the below and rework OverlayPreference to have a 'entities' similar to a list preference, these would be the 'display values' + val availableDownLoads = mutableListOf().apply { + addAll(layerLocalDataSource.readByEvent(event, "GeoPackage")) + addAll(layerLocalDataSource.readByEvent(event, "Feature")) + addAll(layerLocalDataSource.readByEvent(event, "Imagery")) + }.any { !it.isLoaded } + + val overlayPreference = findPreference(resources.getString(R.string.tileOverlaysKey)) as OverlayPreference? + overlayPreference?.setAvailableDownloads(availableDownLoads) + } + + override fun onPause() { + super.onPause() + findPreference(getString(R.string.tileOverlaysKey))?.onPreferenceClickListener = null + findPreference(getString(R.string.onlineLayersKey))?.onPreferenceClickListener = null + } + + private fun onFeeds(feeds: List) { + val screen = preferenceScreen + val enabledFeeds = mapLayerPreferences.getEnabledFeeds(event?.id) + for (feed in feeds) { + val category = screen.getPreference(screen.preferenceCount - 1) as PreferenceCategory + val feedPreference = mapLayerPreferences.mapFeedPreference( + feed, + requireActivity(), + enabledFeeds.contains(feed.id) + ) + feedPreference.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + onFeedClick(feed, feedPreference.isChecked) + true + } + category.addPreference(feedPreference) + } + } + + private fun onFeedClick(feed: Feed, on: Boolean) { + val feeds = mapLayerPreferences.getEnabledFeeds(event?.id).toMutableSet() + if (on) { + feeds.add(feed.id) + } else { + feeds.remove(feed.id) + } + mapLayerPreferences.setEnabledFeeds(event?.id, feeds) + } + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportFragmentManager.beginTransaction().replace(android.R.id.content, preference).commit() + } + + companion object { + const val TILE_OVERLAY_ACTIVITY = 100 + const val ONLINE_LAYERS_OVERLAY_ACTIVITY = 200 + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesViewModel.kt index 46865ace9..d64afd9c9 100644 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/MapPreferencesViewModel.kt @@ -1,28 +1,25 @@ package mil.nga.giat.mage.map.preference -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Transformations import androidx.lifecycle.ViewModel +import androidx.lifecycle.switchMap import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import mil.nga.giat.mage.data.feed.Feed -import mil.nga.giat.mage.data.feed.FeedDao +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.dao.feed.FeedDao import javax.inject.Inject @HiltViewModel class MapPreferencesViewModel @Inject constructor( - @ApplicationContext val context: Context, private val feedDao: FeedDao ): ViewModel() { private val eventId = MutableLiveData() - val feeds: LiveData> = Transformations.switchMap(eventId) { + val feeds: LiveData> = eventId.switchMap { feedDao.mappableFeeds(it) } - fun setEvent(eventId: String) { + fun setEvent(eventId: String?) { this.eventId.value = eventId } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.java b/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.java deleted file mode 100644 index c61fc47ef..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.java +++ /dev/null @@ -1,530 +0,0 @@ -package mil.nga.giat.mage.map.preference; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Environment; -import android.text.format.Formatter; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.BaseExpandableListAdapter; -import android.widget.Checkable; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.MainThread; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.List; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.map.cache.CacheOverlay; -import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.map.cache.GeoPackageCacheOverlay; -import mil.nga.giat.mage.map.cache.StaticFeatureCacheOverlay; -import mil.nga.giat.mage.map.download.GeoPackageDownloadManager; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.fetch.StaticFeatureServerFetch; -import mil.nga.giat.mage.utils.ByteUtils; - -/** - * Adapter used to control the downloadable layers - * - *

- * ALL public methods MUST be made on the UI thread to ensure concurrency. - */ -@MainThread -public class OfflineLayersAdapter extends BaseExpandableListAdapter { - - /** - * log identifier - */ - private static final String LOG_NAME = OfflineLayersAdapter.class.getName(); - - /** - * Context - */ - private final Context context; - - /** - * List of geopackage and static feature cache overlays (downloaded) - */ - private final List cacheOverlays = new ArrayList<>(); - - /** - * Sideloaded layers. - */ - private final List sideloadedOverlays = new ArrayList<>(); - - /** - * Layers that can be downloaded - */ - private final List downloadableLayers = new ArrayList<>(); - - - private final GeoPackageDownloadManager downloadManager; - - - /** - * Constructor - * - * @param activity - */ - public OfflineLayersAdapter(Context activity, GeoPackageDownloadManager downloadManager) { - this.context = activity; - this.downloadManager = downloadManager; - } - - /** - * Call when a layer is downloaded and turned into a cache overlay - * - * @param overlay the downloaded overlay - * @param layer the layer to remove - */ - public void addOverlay(CacheOverlay overlay, Layer layer) { - - if(overlay instanceof GeoPackageCacheOverlay || overlay instanceof StaticFeatureCacheOverlay) { - if (layer.isLoaded()) { - downloadableLayers.remove(layer); - cacheOverlays.add(overlay); - } - } - } - - /** - * This is the live list of downloadable layers and any actions on - * this list should be synchronized - * - * @return - */ - public List getDownloadableLayers() { - return downloadableLayers; - } - - /** - * This is the live list of overlays and any actions on - * this list should be synchronized - * - * @return - */ - public List getOverlays() {return this.cacheOverlays;} - - public List getSideloadedOverlays() {return this.sideloadedOverlays; } - - public void updateDownloadProgress(View view, Layer layer) { - int progress = downloadManager.getProgress(layer); - long size = layer.getFileSize(); - - final ProgressBar progressBar = view.findViewById(R.id.layer_progress); - final View download = view.findViewById(R.id.layer_download); - - if (progress <= 0) { - String reason = downloadManager.isFailed(layer); - if(!StringUtils.isEmpty(reason)) { - Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); - progressBar.setVisibility(View.GONE); - download.setVisibility(View.VISIBLE); - } - return; - } - - int currentProgress = (int) (progress / (float) size * 100); - progressBar.setIndeterminate(false); - progressBar.setProgress(currentProgress); - - TextView layerSize = view.findViewById(R.id.layer_size); - layerSize.setText(String.format("Downloading: %s of %s", - Formatter.formatFileSize(context, progress), - Formatter.formatFileSize(context, size))); - } - - @Override - public int getGroupCount() { - return cacheOverlays.size() + sideloadedOverlays.size() + downloadableLayers.size(); - } - - @Override - public int getChildrenCount(int i) { - int children = 0; - - if(!cacheOverlays.isEmpty() && i < cacheOverlays.size() ){ - children = cacheOverlays.get(i).getChildren().size(); - } else if(!sideloadedOverlays.isEmpty() && i - cacheOverlays.size() < sideloadedOverlays.size()){ - children = sideloadedOverlays.get(i - cacheOverlays.size()).getChildren().size(); - } - - for (Layer layer : downloadableLayers) { - if(layer.getType().equalsIgnoreCase("geopackage")) { - if (layer.isLoaded()) { - children++; - } - } - } - - return children; - } - - @Override - public Object getGroup(int i) { - Object group = null; - if (!cacheOverlays.isEmpty() && i < cacheOverlays.size()) { - group = cacheOverlays.get(i); - } else if(!sideloadedOverlays.isEmpty() && i - cacheOverlays.size() < sideloadedOverlays.size()) { - group = sideloadedOverlays.get(i - cacheOverlays.size()); - } else { - group = downloadableLayers.get(i - cacheOverlays.size() - sideloadedOverlays.size()); - } - return group; - } - - @Override - public Object getChild(int i, int j) { - Object child = null; - if (!cacheOverlays.isEmpty() && i < cacheOverlays.size()) { - child = cacheOverlays.get(i).getChildren().get(j); - } else if (!sideloadedOverlays.isEmpty() && i - cacheOverlays.size() < sideloadedOverlays.size()) { - child = sideloadedOverlays.get(i - cacheOverlays.size()).getChildren().get(j); - } - - return child; - } - - @Override - public long getGroupId(int i) { - return i; - } - - @Override - public long getChildId(int i, int j) { - return j; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public View getGroupView(int i, boolean isExpanded, View view, ViewGroup viewGroup) { - if (!cacheOverlays.isEmpty() && i < cacheOverlays.size()) { - return getOverlayView(i, isExpanded, view, viewGroup); - } else if(!sideloadedOverlays.isEmpty() && i - cacheOverlays.size() < sideloadedOverlays.size()) { - return getOverlaySideloadedView(i, isExpanded, view, viewGroup); - }else { - return getDownloadableLayerView(i, isExpanded, view, viewGroup); - } - } - - private View getOverlayView(int i, boolean isExpanded, View view, ViewGroup viewGroup) { - LayoutInflater inflater = LayoutInflater.from(context); - view = inflater.inflate(R.layout.offline_layer_group, viewGroup, false); - final CacheOverlay overlay = cacheOverlays.get(i); - - TextView groupView = view.findViewById(R.id.cache_over_group_text); - Event event = EventHelper.getInstance(context).getCurrentEvent(); - groupView.setText(event.getName() + " Layers"); - - view.findViewById(R.id.section_header).setVisibility(i == 0 ? View.VISIBLE : View.GONE); - - ImageView imageView = view.findViewById(R.id.cache_overlay_group_image); - TextView cacheName = view.findViewById(R.id.cache_overlay_group_name); - TextView childCount = view.findViewById(R.id.cache_overlay_group_count); - View checkable = view.findViewById(R.id.cache_overlay_group_checkbox); - - checkable.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean checked = ((Checkable) v).isChecked(); - - overlay.setEnabled(checked); - - boolean modified = false; - for (CacheOverlay childCache : overlay.getChildren()) { - if (childCache.isEnabled() != checked) { - childCache.setEnabled(checked); - modified = true; - } - } - - if (modified) { - notifyDataSetChanged(); - } - } - }); - - Integer imageResource = overlay.getIconImageResourceId(); - if (imageResource != null) { - imageView.setImageResource(imageResource); - } - - Layer layer = null; - if (overlay instanceof GeoPackageCacheOverlay) { - String filePath = ((GeoPackageCacheOverlay) overlay).getFilePath(); - if (filePath.startsWith(String.format("%s/MAGE/geopackages", context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)))) { - try { - String relativePath = filePath.split(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/")[1]; - layer = LayerHelper.getInstance(context.getApplicationContext()).getByRelativePath(relativePath); - } catch (Exception e) { - Log.e(LOG_NAME, "Error getting layer by relative path", e); - } - } - } - cacheName.setText(layer != null ? layer.getName() : overlay.getName()); - - if (overlay.isSupportsChildren()) { - childCount.setText("(" + getChildrenCount(i) + " layers)"); - } else { - childCount.setText(""); - } - ((Checkable) checkable).setChecked(overlay.isEnabled()); - - return view; - } - - private View getOverlaySideloadedView(int i, boolean isExpanded, View view, ViewGroup viewGroup) { - LayoutInflater inflater = LayoutInflater.from(context); - view = inflater.inflate(R.layout.offline_layer_sideloaded, viewGroup, false); - final CacheOverlay overlay = sideloadedOverlays.get(i - cacheOverlays.size()); - - TextView groupView = view.findViewById(R.id.cache_overlay_side_group_text); - - groupView.setText("My Layers"); - - view.findViewById(R.id.section_header).setVisibility(i - cacheOverlays.size() == 0 ? View.VISIBLE : View.GONE); - - ImageView imageView = view.findViewById(R.id.cache_overlay_side_group_image); - TextView cacheName = view.findViewById(R.id.cache_overlay_side_group_name); - TextView childCount = view.findViewById(R.id.cache_overlay_side_group_count); - View checkable = view.findViewById(R.id.cache_overlay_side_group_checkbox); - - checkable.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean checked = ((Checkable) v).isChecked(); - - overlay.setEnabled(checked); - - boolean modified = false; - for (CacheOverlay childCache : overlay.getChildren()) { - if (childCache.isEnabled() != checked) { - childCache.setEnabled(checked); - modified = true; - } - } - - if (modified) { - notifyDataSetChanged(); - } - } - }); - - Integer imageResource = overlay.getIconImageResourceId(); - if (imageResource != null) { - imageView.setImageResource(imageResource); - } - - Layer layer = null; - if (overlay instanceof GeoPackageCacheOverlay) { - String filePath = ((GeoPackageCacheOverlay) overlay).getFilePath(); - if (filePath.startsWith(String.format("%s/MAGE/geopackages", context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)))) { - try { - String relativePath = filePath.split(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/")[1]; - layer = LayerHelper.getInstance(context.getApplicationContext()).getByRelativePath(relativePath); - } catch (Exception e) { - Log.e(LOG_NAME, "Error getting layer by relative path", e); - } - } - } - cacheName.setText(layer != null ? layer.getName() : overlay.getName()); - - if (overlay.isSupportsChildren()) { - childCount.setText("(" + getChildrenCount(i) + " layers)"); - } else { - childCount.setText(""); - } - ((Checkable) checkable).setChecked(overlay.isEnabled()); - - return view; - } - - private View getDownloadableLayerView(int i, boolean isExpanded, View view, ViewGroup viewGroup) { - LayoutInflater inflater = LayoutInflater.from(context); - view = inflater.inflate(R.layout.layer_overlay, viewGroup, false); - - Layer layer = downloadableLayers.get(i - cacheOverlays.size() - sideloadedOverlays.size()); - - view.findViewById(R.id.section_header).setVisibility(i - cacheOverlays.size() - sideloadedOverlays.size() == 0 ? View.VISIBLE : View.GONE); - - TextView cacheName = view.findViewById(R.id.layer_name); - cacheName.setText(layer.getName()); - TextView description = view.findViewById(R.id.layer_description); - - if (layer.getType().equalsIgnoreCase("geopackage")) { - description.setText(ByteUtils.getInstance().getDisplay(layer.getFileSize(), true)); - } else { - description.setText("Static feature data"); - } - - final ProgressBar progressBar = view.findViewById(R.id.layer_progress); - final View download = view.findViewById(R.id.layer_download); - if (layer.getType().equalsIgnoreCase("geopackage")) { - if (downloadManager.isDownloading(layer)) { - int progress = downloadManager.getProgress(layer); - long fileSize = layer.getFileSize(); - progressBar.setVisibility(View.VISIBLE); - download.setVisibility(View.GONE); - - view.setEnabled(false); - view.setOnClickListener(null); - - int currentProgress = (int) (progress / (float) layer.getFileSize() * 100); - progressBar.setIndeterminate(false); - progressBar.setProgress(currentProgress); - - TextView layerSize = view.findViewById(R.id.layer_size); - layerSize.setVisibility(View.VISIBLE); - layerSize.setText(String.format("Downloading: %s of %s", - Formatter.formatFileSize(context, progress), - Formatter.formatFileSize(context, fileSize))); - } else { - String reason = downloadManager.isFailed(layer); - if(!StringUtils.isEmpty(reason)) { - Toast.makeText(context, reason, Toast.LENGTH_LONG).show(); - } - progressBar.setVisibility(View.GONE); - download.setVisibility(View.VISIBLE); - } - } else if (layer.getType().equalsIgnoreCase("feature")) { - if (!layer.isLoaded() && layer.getDownloadId() == null) { - download.setVisibility(View.VISIBLE); - progressBar.setVisibility(View.GONE); - }else { - download.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - progressBar.setIndeterminate(true); - } - } - - final Layer threadLayer = layer; - download.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - download.setVisibility(View.GONE); - progressBar.setIndeterminate(true); - progressBar.setVisibility(View.VISIBLE); - - if (threadLayer.getType().equalsIgnoreCase("geopackage")) { - downloadManager.downloadGeoPackage(threadLayer); - } else if (threadLayer.getType().equalsIgnoreCase("feature")) { - @SuppressLint("StaticFieldLeak") AsyncTask fetcher = - new AsyncTask() { - @Override - protected Layer doInBackground(Layer... layers) { - StaticFeatureServerFetch staticFeatureServerFetch = - new StaticFeatureServerFetch(context); - try { - staticFeatureServerFetch.load(null, layers[0]); - } catch (Exception e) { - Log.w(LOG_NAME, "Error fetching static layers", e); - } - return layers[0]; - } - - @Override - protected void onPostExecute(Layer layer) { - super.onPostExecute(layer); - OfflineLayersAdapter.this.getDownloadableLayers().remove(layer); - OfflineLayersAdapter.this.getOverlays().clear(); - OfflineLayersAdapter.this.getSideloadedOverlays().clear(); - notifyDataSetChanged(); - CacheProvider.getInstance(context).refreshTileOverlays(); - } - }; - fetcher.execute(threadLayer); - } - } - }); - - view.setTag(layer.getName()); - - return view; - } - - @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, - View convertView, ViewGroup parent) { - - if (convertView == null) { - LayoutInflater inflater = LayoutInflater.from(context); - convertView = inflater.inflate(R.layout.offline_layer_child, parent, false); - } - - final CacheOverlay overlay = groupPosition < cacheOverlays.size() - ? cacheOverlays.get(groupPosition) : sideloadedOverlays.get(groupPosition - cacheOverlays.size()); - final CacheOverlay childCache = overlay.getChildren().get(childPosition); - - ImageView imageView = convertView.findViewById(R.id.cache_overlay_child_image); - TextView tableName = convertView.findViewById(R.id.cache_overlay_child_name); - TextView info = convertView.findViewById(R.id.cache_overlay_child_info); - View checkBox = convertView.findViewById(R.id.cache_overlay_child_checkbox); - - convertView.findViewById(R.id.divider).setVisibility(isLastChild ? View.VISIBLE : View.INVISIBLE); - - checkBox.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean checked = ((Checkable) v).isChecked(); - - childCache.setEnabled(checked); - - boolean modified = false; - if (checked) { - if (!overlay.isEnabled()) { - overlay.setEnabled(true); - modified = true; - } - } else if (overlay.isEnabled()) { - modified = true; - for (CacheOverlay childCache : overlay.getChildren()) { - if (childCache.isEnabled()) { - modified = false; - break; - } - } - if (modified) { - overlay.setEnabled(false); - } - } - - if (modified) { - notifyDataSetChanged(); - } - } - }); - - tableName.setText(childCache.getName()); - info.setText(childCache.getInfo()); - ((Checkable)checkBox).setChecked(childCache.isEnabled()); - - Integer imageResource = childCache.getIconImageResourceId(); - if (imageResource != null){ - imageView.setImageResource(imageResource); - } - - return convertView; - } - - @Override - public boolean isChildSelectable(int i, int j) { - return true; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.kt new file mode 100644 index 000000000..f4378a16f --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/OfflineLayersAdapter.kt @@ -0,0 +1,417 @@ +package mil.nga.giat.mage.map.preference + +import android.content.Context +import android.os.Environment +import android.text.format.Formatter +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseExpandableListAdapter +import android.widget.Checkable +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import com.google.android.material.progressindicator.LinearProgressIndicator +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.data.repository.layer.LayerRepository +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.map.cache.CacheOverlay +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.map.cache.GeoPackageCacheOverlay +import mil.nga.giat.mage.map.cache.StaticFeatureCacheOverlay +import mil.nga.giat.mage.map.download.GeoPackageDownloadManager +import mil.nga.giat.mage.utils.ByteUtils +import org.apache.commons.lang3.StringUtils + + +class OfflineLayersAdapter( + private val context: Context, + private val cacheProvider: CacheProvider, + private val downloadManager: GeoPackageDownloadManager, + private val layerRepository: LayerRepository, + private val layerLocalDataSource: LayerLocalDataSource, + private val event: Event? +) : BaseExpandableListAdapter() { + + val overlays: MutableList = ArrayList() + val sideloadedOverlays: MutableList = ArrayList() + val downloadableLayers: MutableList = ArrayList() + + fun addOverlay(overlay: CacheOverlay?, layer: Layer) { + if (overlay is GeoPackageCacheOverlay || overlay is StaticFeatureCacheOverlay) { + if (layer.isLoaded) { + downloadableLayers.remove(layer) + overlays.add(overlay) + } + } + } + + fun updateDownloadProgress(view: View, layer: Layer) { + val progress = downloadManager.getProgress(layer) + val size = layer.fileSize + val progressBar = view.findViewById(R.id.layer_progress) + val download = view.findViewById(R.id.layer_download) + if (progress <= 0) { + val reason = downloadManager.isFailed(layer) + if (!StringUtils.isEmpty(reason)) { + Toast.makeText(context, reason, Toast.LENGTH_LONG).show() + progressBar.visibility = View.GONE + download.visibility = View.VISIBLE + } + return + } + val currentProgress = (progress / size.toFloat() * 100).toInt() + progressBar.isIndeterminate = false + progressBar.progress = currentProgress + val layerSize = view.findViewById(R.id.layer_size) + layerSize.text = String.format( + "Downloading: %s of %s", + Formatter.formatFileSize(context, progress.toLong()), + Formatter.formatFileSize(context, size) + ) + } + + override fun getGroupCount(): Int { + return overlays.size + sideloadedOverlays.size + downloadableLayers.size + } + + override fun getChildrenCount(i: Int): Int { + var children = 0 + if (overlays.isNotEmpty() && i < overlays.size) { + children = overlays[i].children.size + } else if (sideloadedOverlays.isNotEmpty() && i - overlays.size < sideloadedOverlays.size) { + children = sideloadedOverlays[i - overlays.size].children.size + } + for (layer: Layer in downloadableLayers) { + if (layer.type.equals("geopackage", ignoreCase = true)) { + if (layer.isLoaded) { + children++ + } + } + } + return children + } + + override fun getGroup(i: Int): Any { + val group = if (overlays.isNotEmpty() && i < overlays.size) { + overlays[i] + } else if (sideloadedOverlays.isNotEmpty() && i - overlays.size < sideloadedOverlays.size) { + sideloadedOverlays[i - overlays.size] + } else { + downloadableLayers[i - overlays.size - sideloadedOverlays.size] + } + return group + } + + override fun getChild(i: Int, j: Int): Any { + var child: Any? = null + if (overlays.isNotEmpty() && i < overlays.size) { + child = overlays[i].children[j] + } else if (sideloadedOverlays.isNotEmpty() && i - overlays.size < sideloadedOverlays.size) { + child = sideloadedOverlays[i - overlays.size].children[j] + } + return (child)!! + } + + override fun getGroupId(i: Int): Long { + return i.toLong() + } + + override fun getChildId(i: Int, j: Int): Long { + return j.toLong() + } + + override fun hasStableIds(): Boolean { + return false + } + + override fun getGroupView( + groupPosition: Int, + isExpanded: Boolean, + convertView: View?, + parent: ViewGroup? + ): View { + if (overlays.isNotEmpty() && groupPosition < overlays.size) { + return getOverlayView(groupPosition, parent) + } else return if (sideloadedOverlays.isNotEmpty() && groupPosition - overlays.size < sideloadedOverlays.size) { + getOverlaySideloadedView(groupPosition, parent) + } else { + getDownloadableLayerView(groupPosition, parent) + } + } + + private fun getOverlayView( + i: Int, + viewGroup: ViewGroup? + ): View { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.offline_layer_group, viewGroup, false) + val overlay = overlays[i] + val groupView = view.findViewById(R.id.cache_over_group_text) + groupView.text = event?.name + " Layers" + view.findViewById(R.id.section_header).visibility = if (i == 0) View.VISIBLE else View.GONE + val imageView = view.findViewById(R.id.cache_overlay_group_image) + val cacheName = view.findViewById(R.id.cache_overlay_group_name) + val childCount = view.findViewById(R.id.cache_overlay_group_count) + val checkable = view.findViewById(R.id.cache_overlay_group_checkbox) + checkable.setOnClickListener { v -> + val checked = (v as Checkable).isChecked + overlay.isEnabled = checked + var modified = false + for (childCache: CacheOverlay in overlay.children) { + if (childCache.isEnabled != checked) { + childCache.isEnabled = checked + modified = true + } + } + if (modified) { + notifyDataSetChanged() + } + } + val imageResource = overlay.iconImageResourceId + if (imageResource != null) { + imageView.setImageResource(imageResource) + } + var layer: Layer? = null + if (overlay is GeoPackageCacheOverlay) { + val filePath = overlay.filePath + if (filePath.startsWith( + String.format( + "%s/MAGE/geopackages", + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + ) + ) + ) { + try { + val relativePath = filePath.split( + (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!! + .absolutePath + "/").toRegex() + ).dropLastWhile { it.isEmpty() }.toTypedArray()[1] + layer = layerLocalDataSource.getByRelativePath(relativePath) + } catch (e: Exception) { + Log.e(LOG_NAME, "Error getting layer by relative path", e) + } + } + } + cacheName.text = if (layer != null) layer.name else overlay.name + if (overlay.isSupportsChildren) { + childCount.text = "(" + getChildrenCount(i) + " layers)" + } else { + childCount.text = "" + } + (checkable as Checkable).isChecked = overlay.isEnabled + return view + } + + private fun getOverlaySideloadedView( + i: Int, + viewGroup: ViewGroup? + ): View { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.offline_layer_sideloaded, viewGroup, false) + val overlay = sideloadedOverlays[i - overlays.size] + val groupView = view.findViewById(R.id.cache_overlay_side_group_text) + groupView.text = "My Layers" + view.findViewById(R.id.section_header).visibility = + if (i - overlays.size == 0) View.VISIBLE else View.GONE + val imageView = view.findViewById(R.id.cache_overlay_side_group_image) + val cacheName = view.findViewById(R.id.cache_overlay_side_group_name) + val childCount = view.findViewById(R.id.cache_overlay_side_group_count) + val checkable = view.findViewById(R.id.cache_overlay_side_group_checkbox) + checkable.setOnClickListener { v -> + val checked = (v as Checkable).isChecked + overlay.isEnabled = checked + var modified = false + for (childCache: CacheOverlay in overlay.children) { + if (childCache.isEnabled != checked) { + childCache.isEnabled = checked + modified = true + } + } + if (modified) { + notifyDataSetChanged() + } + } + val imageResource = overlay.iconImageResourceId + if (imageResource != null) { + imageView.setImageResource(imageResource) + } + var layer: Layer? = null + if (overlay is GeoPackageCacheOverlay) { + val filePath = overlay.filePath + if (filePath.startsWith( + String.format( + "%s/MAGE/geopackages", + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + ) + ) + ) { + try { + val relativePath = filePath.split( + (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!! + .absolutePath + "/").toRegex() + ).dropLastWhile { it.isEmpty() }.toTypedArray()[1] + layer = layerLocalDataSource.getByRelativePath(relativePath) + } catch (e: Exception) { + Log.e(LOG_NAME, "Error getting layer by relative path", e) + } + } + } + cacheName.text = if (layer != null) layer.name else overlay.name + if (overlay.isSupportsChildren) { + childCount.text = "(" + getChildrenCount(i) + " layers)" + } else { + childCount.text = "" + } + (checkable as Checkable).isChecked = overlay.isEnabled + return view + } + + private fun getDownloadableLayerView( + i: Int, + viewGroup: ViewGroup? + ): View { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.layer_overlay, viewGroup, false) + val layer = downloadableLayers[i - overlays.size - sideloadedOverlays.size] + view.findViewById(R.id.section_header).visibility = + if (i - overlays.size - sideloadedOverlays.size == 0) View.VISIBLE else View.GONE + val cacheName = view.findViewById(R.id.layer_name) + cacheName.text = layer.name + val description = view.findViewById(R.id.layer_description) + if (layer.type.equals("geopackage", ignoreCase = true)) { + description.text = ByteUtils.getInstance().getDisplay(layer.fileSize, true) + } else { + description.text = "Static feature data" + } + val progressBar = view.findViewById(R.id.layer_progress) + val download = view.findViewById(R.id.layer_download) + if (layer.type.equals("geopackage", ignoreCase = true)) { + if (downloadManager.isDownloading(layer)) { + val progress = downloadManager.getProgress(layer) + val fileSize = layer.fileSize + progressBar.visibility = View.VISIBLE + download.visibility = View.GONE + view.isEnabled = false + view.setOnClickListener(null) + val currentProgress = (progress / layer.fileSize.toFloat() * 100).toInt() + progressBar.isIndeterminate = false + progressBar.progress = currentProgress + val layerSize = view.findViewById(R.id.layer_size) + layerSize.visibility = View.VISIBLE + layerSize.text = String.format( + "Downloading: %s of %s", + Formatter.formatFileSize(context, progress.toLong()), + Formatter.formatFileSize(context, fileSize) + ) + } else { + val reason = downloadManager.isFailed(layer) + if (!StringUtils.isEmpty(reason)) { + Toast.makeText(context, reason, Toast.LENGTH_LONG).show() + } + progressBar.visibility = View.GONE + download.visibility = View.VISIBLE + } + } else if (layer.type.equals("feature", ignoreCase = true)) { + if (!layer.isLoaded && layer.downloadId == null) { + download.visibility = View.VISIBLE + progressBar.visibility = View.GONE + } else { + download.visibility = View.GONE + progressBar.visibility = View.VISIBLE + progressBar.isIndeterminate = true + } + } + download.setOnClickListener { + download.visibility = View.GONE + progressBar.isIndeterminate = true + progressBar.visibility = View.VISIBLE + if (layer.type.equals("geopackage", ignoreCase = true)) { + downloadManager.downloadGeoPackage(event, layer) + } else if (layer.type.equals("feature", ignoreCase = true)) { + CoroutineScope(Dispatchers.IO).launch { + try { + cacheProvider.refreshTileOverlays() + layerRepository.loadFeatures(layer) + + CoroutineScope(Dispatchers.Main).launch { + downloadableLayers.remove(layer) + overlays.clear() + sideloadedOverlays.clear() + notifyDataSetChanged() + } + } catch (e: Exception) { + Log.w(LOG_NAME, "Error fetching static layers", e) + } + } + } + } + view.tag = layer.name + return view + } + + override fun getChildView( + groupPosition: Int, + childPosition: Int, + isLastChild: Boolean, + convertView: View?, + parent: ViewGroup + ): View { + val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.offline_layer_child, parent, false) + + val overlay = if (groupPosition < overlays.size) overlays[groupPosition] else sideloadedOverlays[groupPosition - overlays.size] + val childCache = overlay.children[childPosition] + val imageView = view?.findViewById(R.id.cache_overlay_child_image) + val tableName = view?.findViewById(R.id.cache_overlay_child_name) + val info = view?.findViewById(R.id.cache_overlay_child_info) + val checkBox = view?.findViewById(R.id.cache_overlay_child_checkbox) + view?.findViewById(R.id.divider)?.visibility = if (isLastChild) View.VISIBLE else View.INVISIBLE + checkBox?.setOnClickListener { v -> + val checked = (v as Checkable).isChecked + childCache.isEnabled = checked + var modified = false + if (checked) { + if (!overlay.isEnabled) { + overlay.isEnabled = true + modified = true + } + } else if (overlay.isEnabled) { + modified = true + for (childCache: CacheOverlay in overlay.children) { + if (childCache.isEnabled) { + modified = false + break + } + } + if (modified) { + overlay.isEnabled = false + } + } + if (modified) { + notifyDataSetChanged() + } + } + tableName?.text = childCache.name + info?.text = childCache.info + (checkBox as Checkable).isChecked = childCache.isEnabled + val imageResource = childCache.iconImageResourceId + if (imageResource != null) { + imageView?.setImageResource(imageResource) + } + return view + } + + override fun isChildSelectable(i: Int, j: Int): Boolean { + return true + } + + companion object { + private val LOG_NAME = OfflineLayersAdapter::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.java b/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.java deleted file mode 100644 index ff7b030ac..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.java +++ /dev/null @@ -1,460 +0,0 @@ -package mil.nga.giat.mage.map.preference; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Parcelable; -import android.preference.PreferenceManager; - -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.URLUtil; -import android.widget.Checkable; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.map.cache.CacheOverlay; -import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.map.cache.URLCacheOverlay; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.fetch.ImageryServerFetch; - -/** - * This activity is the view component for online layers - * - */ -public class OnlineLayersPreferenceActivity extends AppCompatActivity { - - /** - * logger - */ - private static final String LOG_NAME = OnlineLayersPreferenceActivity.class.getName(); - - /** - * Fragment showing the actual online layers URLs - */ - private OnlineLayersListFragment onlineLayersFragment; - - private static SharedPreferences ourSharedPreferences; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_online_layers); - - onlineLayersFragment = (OnlineLayersListFragment) getSupportFragmentManager().findFragmentById(R.id.online_layers_fragment); - ourSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - @Override - public void onBackPressed() { - SharedPreferences.Editor editor = ourSharedPreferences.edit(); - editor.putStringSet(getResources().getString(R.string.onlineLayersKey), new HashSet<>(onlineLayersFragment.getSelectedOverlays())); - editor.commit(); - - finish(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public static class OnlineLayersListFragment extends Fragment implements CacheProvider.OnCacheOverlayListener { - - /** - * This class is synchronized by only being accessed on the UI thread - */ - private OnlineLayersAdapter adapter; - - private MenuItem refreshButton; - private View contentView; - private View noContentView; - private RecyclerView recyclerView; - private SwipeRefreshLayout swipeContainer; - private Parcelable listState; - - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_online_layers, container, false); - - contentView = view.findViewById(R.id.online_layers_content); - noContentView = view.findViewById(R.id.online_layers_no_content); - - setHasOptionsMenu(true); - - swipeContainer = view.findViewById(R.id.swipeContainer); - swipeContainer.setColorSchemeResources(R.color.md_blue_600, R.color.md_orange_A200); - swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - softRefresh(); - hardRefresh(); - } - }); - - recyclerView = view.findViewById(R.id.recycler_view); - recyclerView.setTag("online"); - - RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity()); - recyclerView.setLayoutManager(mLayoutManager); - recyclerView.setItemAnimator(new DefaultItemAnimator()); - - adapter = new OnlineLayersAdapter(getActivity()); - - return view; - } - - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.online_layers_menu, menu); - - refreshButton = menu.findItem(R.id.online_layers_refresh); - refreshButton.setEnabled(false); - - CacheProvider.getInstance(getActivity()).registerCacheOverlayListener(this, false); - softRefresh(); - CacheProvider.getInstance(getContext()).refreshTileOverlays(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.online_layers_refresh: - softRefresh(); - hardRefresh(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void softRefresh(){ - refreshButton.setEnabled(false); - swipeContainer.setRefreshing(true); - - adapter.clear(); - adapter.notifyDataSetChanged(); - - SharedPreferences.Editor editor = ourSharedPreferences.edit(); - editor.putStringSet(getResources().getString(R.string.onlineLayersKey), new HashSet<>(getSelectedOverlays())); - editor.commit(); - } - - private void hardRefresh(){ - if (getActivity() != null) { - final Context c = getActivity().getApplicationContext(); - Runnable runnable = new Runnable() { - @Override - public void run() { - ImageryServerFetch imageryServerFetch = new ImageryServerFetch(c); - try { - imageryServerFetch.fetch(); - - SharedPreferences.Editor editor = ourSharedPreferences.edit(); - editor.putStringSet(getResources().getString(R.string.onlineLayersKey), new HashSet<>(getSelectedOverlays())); - editor.commit(); - - CacheProvider.getInstance(getContext()).refreshTileOverlays(); - } catch (Exception e) { - Log.w(LOG_NAME, "Failed fetching imagery", e); - } - } - }; - - new Thread(runnable).start(); - } else { - Log.e(LOG_NAME, "Activity is null"); - } - } - - @Override - public void onCacheOverlay(final List cacheOverlays) { - if(getActivity() == null) { - Log.w(LOG_NAME, "Failed to handle new cache overly since activity was null"); - return; - } - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - Collection layers = new ArrayList<>(); - - try { - layers = LayerHelper.getInstance(getActivity()).readByEvent(EventHelper.getInstance(getActivity()).getCurrentEvent(), "Imagery"); - } catch (Exception e) { - Log.e(LOG_NAME, "Problem getting layers.", e); - } - - - adapter.clear(); - adapter.notifyDataSetChanged(); - - List secureLayers = new ArrayList<>(); - List insecureLayers = new ArrayList<>(); - - // Set what should be checked based on preferences. - Set overlays = ourSharedPreferences.getStringSet(getResources().getString(R.string.onlineLayersKey), Collections.emptySet()); - for (Layer layer : layers) { - boolean enabled = overlays != null ? overlays.contains(layer.getName()) : false; - - CacheOverlay overlay = CacheProvider.getInstance(getContext()).getOverlay(layer.getName()); - if (overlay != null) { - overlay.setEnabled(enabled); - } - - if (URLUtil.isHttpsUrl(layer.getUrl())) { - secureLayers.add(layer); - } else { - insecureLayers.add(layer); - } - } - - Comparator compare = new Comparator() { - @Override - public int compare(Layer o1, Layer o2) { - return o1.getName().compareTo(o2.getName()); - - } - }; - - Collections.sort(secureLayers, compare); - Collections.sort(insecureLayers, compare); - - adapter.addAllNonLayers(insecureLayers); - adapter.addAllSecureLayers(secureLayers); - adapter.notifyDataSetChanged(); - - if (!layers.isEmpty()) { - noContentView.setVisibility(View.GONE); - contentView.setVisibility(View.VISIBLE); - } else { - contentView.setVisibility(View.GONE); - noContentView.setVisibility(View.VISIBLE); - } - - refreshButton.setEnabled(true); - swipeContainer.setRefreshing(false); - } - }); - } - - public ArrayList getSelectedOverlays() { - ArrayList overlays = new ArrayList<>(); - for (CacheOverlay overlay : CacheProvider.getInstance(getContext()).getCacheOverlays()) { - if (overlay instanceof URLCacheOverlay) { - if (overlay.isEnabled()) { - overlays.add(overlay.getName()); - } - } - } - - return overlays; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - CacheProvider.getInstance(getActivity()).unregisterCacheOverlayListener(this); - } - - @Override - public void onResume() { - super.onResume(); - recyclerView.setAdapter(adapter); - - if (listState != null) { - recyclerView.getLayoutManager().onRestoreInstanceState(listState); - } - } - - @Override - public void onPause() { - super.onPause(); - - listState = recyclerView.getLayoutManager().onSaveInstanceState(); - recyclerView.setAdapter(null); - } - } - - /** - * - *

- * ALL public methods MUST be made on the UI thread to ensure concurrency. - */ - public static class OnlineLayersAdapter extends RecyclerView.Adapter{ - - private static final int ITEM_TYPE_HEADER = 1; - private static final int ITEM_TYPE_LAYER = 2; - - private final Context context; - private final List secureLayers = new ArrayList<>(); - private final List nonSecureLayers = new ArrayList<>(); - - OnlineLayersAdapter(Context context) { - this.context = context; - } - - public void clear(){ - this.secureLayers.clear(); - this.nonSecureLayers.clear(); - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int i) { - if (i == ITEM_TYPE_LAYER) { - View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.online_layers_list_item, parent, false); - return new LayerViewHolder(itemView); - } else if (i == ITEM_TYPE_HEADER) { - View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.event_list_section_header, parent, false); - return new SectionViewHolder(itemView); - } else { - return null; - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int i) { - if (holder instanceof LayerViewHolder) { - bindLayerViewHolder((LayerViewHolder) holder, i); - } else if (holder instanceof SectionViewHolder) { - bindSectionViewHolder((SectionViewHolder) holder, i); - } - } - - private void bindLayerViewHolder(LayerViewHolder holder, int i) { - final View view = holder.itemView; - Layer tmp = null; - - if (i <= secureLayers.size()) { - tmp = secureLayers.get(i - 1); - } else { - tmp = nonSecureLayers.get(i - secureLayers.size() - 2); - } - - final Layer layer = tmp; - - TextView title = view.findViewById(R.id.online_layers_title); - title.setText(layer != null ? layer.getName() : ""); - - TextView summary = view.findViewById(R.id.online_layers_summary); - summary.setText(layer.getUrl()); - - final View sw = view.findViewById(R.id.online_layers_toggle); - - if (URLUtil.isHttpUrl(layer.getUrl())) { - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - new AlertDialog.Builder(context) - .setTitle("Non HTTPS Layer") - .setMessage("We cannot load this layer on mobile because it cannot be accessed securely.") - .setPositiveButton("OK", null).show(); - } - }); - sw.setOnClickListener(null); - sw.setEnabled(false); - ((Checkable) sw).setChecked(false); - } else { - view.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - View toggle = view.findViewById(R.id.online_layers_toggle); - toggle.performClick(); - } - }); - sw.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - boolean isChecked = ((Checkable) v).isChecked(); - - CacheOverlay overlay = CacheProvider.getInstance(context).getOverlay(layer.getName()); - if (overlay != null) { - overlay.setEnabled(isChecked); - } - } - }); - sw.setEnabled(true); - CacheOverlay overlay = CacheProvider.getInstance(context).getOverlay(layer.getName()); - if (overlay != null) { - ((Checkable) sw).setChecked(overlay.isEnabled()); - } - } - } - - private void bindSectionViewHolder(SectionViewHolder holder, int position) { - holder.sectionName.setText(position == 0 ? "Secure Layers" : "Nonsecure Layers"); - } - - public void addAllSecureLayers(List layers){ - this.secureLayers.addAll(layers); - } - - public void addAllNonLayers(List layers){ - this.nonSecureLayers.addAll(layers); - } - - @Override - public int getItemCount() { - return secureLayers.size() + nonSecureLayers.size() + 2; - } - - @Override - public int getItemViewType(int position) { - if (position == 0 || position == secureLayers.size() + 1) { - return ITEM_TYPE_HEADER; - } else { - return ITEM_TYPE_LAYER; - } - } - - private class LayerViewHolder extends RecyclerView.ViewHolder { - LayerViewHolder(View view) { - super(view); - } - } - - private class SectionViewHolder extends RecyclerView.ViewHolder { - private TextView sectionName; - - private SectionViewHolder(View view) { - super(view); - - sectionName = view.findViewById(R.id.section_name); - } - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.kt new file mode 100644 index 000000000..e945f2f7e --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/OnlineLayersPreferenceActivity.kt @@ -0,0 +1,355 @@ +package mil.nga.giat.mage.map.preference + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.os.Parcelable +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.webkit.URLUtil +import android.widget.Checkable +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.repository.layer.LayerRepository +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.map.cache.CacheOverlay +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.map.cache.CacheProvider.OnCacheOverlayListener +import mil.nga.giat.mage.map.cache.URLCacheOverlay +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import java.util.Collections +import javax.inject.Inject + +@AndroidEntryPoint +class OnlineLayersPreferenceActivity : AppCompatActivity() { + @Inject lateinit var prefernces: SharedPreferences + + private var onlineLayersFragment: OnlineLayersListFragment? = null + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_online_layers) + onlineLayersFragment = supportFragmentManager.findFragmentById(R.id.online_layers_fragment) as OnlineLayersListFragment? + } + + override fun onBackPressed() { + val overlays = onlineLayersFragment?.selectedOverlays?.toSet() ?: emptySet() + prefernces + .edit() + .putStringSet(resources.getString(R.string.onlineLayersKey), overlays) + .apply() + + finish() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + onBackPressed() + true + } + + else -> super.onOptionsItemSelected(item) + } + } + + @AndroidEntryPoint + class OnlineLayersListFragment : Fragment(), OnCacheOverlayListener { + @Inject lateinit var preferences: SharedPreferences + @Inject lateinit var cacheProvider: CacheProvider + @Inject lateinit var layerRepository: LayerRepository + @Inject lateinit var layerLocalDataSource: LayerLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + + private var event: Event? = null + private lateinit var adapter: OnlineLayersAdapter + private lateinit var refreshButton: MenuItem + private lateinit var contentView: View + private lateinit var noContentView: View + private lateinit var recyclerView: RecyclerView + private lateinit var swipeContainer: SwipeRefreshLayout + private var listState: Parcelable? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + event = eventLocalDataSource.currentEvent + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_online_layers, container, false) + contentView = view.findViewById(R.id.online_layers_content) + noContentView = view.findViewById(R.id.online_layers_no_content) + setHasOptionsMenu(true) + swipeContainer = view.findViewById(R.id.swipeContainer) + swipeContainer.setColorSchemeResources(R.color.md_blue_600, R.color.md_orange_A200) + swipeContainer.setOnRefreshListener { + softRefresh() + hardRefresh() + } + recyclerView = view.findViewById(R.id.recycler_view) + recyclerView.tag = "online" + val mLayoutManager: RecyclerView.LayoutManager = LinearLayoutManager(activity) + recyclerView.layoutManager = mLayoutManager + recyclerView.itemAnimator = DefaultItemAnimator() + adapter = OnlineLayersAdapter(requireContext().applicationContext, cacheProvider) + return view + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.online_layers_menu, menu) + refreshButton = menu.findItem(R.id.online_layers_refresh) + refreshButton.isEnabled = false + cacheProvider.registerCacheOverlayListener(this, false) + softRefresh() + + CoroutineScope(Dispatchers.IO).launch { + cacheProvider.refreshTileOverlays() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.online_layers_refresh) { + softRefresh() + hardRefresh() + return true + } + return super.onOptionsItemSelected(item) + } + + private fun softRefresh() { + refreshButton.isEnabled = false + swipeContainer.isRefreshing = true + adapter.clear() + adapter.notifyDataSetChanged() + + preferences + .edit() + .putStringSet(resources.getString(R.string.onlineLayersKey), selectedOverlays.toSet()) + .apply() + } + + private fun hardRefresh() { + CoroutineScope(Dispatchers.IO).launch { + try { + layerRepository.fetchImageryLayers() + preferences + .edit() + .putStringSet(resources.getString(R.string.onlineLayersKey), selectedOverlays.toSet()) + .apply() + cacheProvider.refreshTileOverlays() + } catch (e: Exception) { + Log.w(LOG_NAME, "Failed fetching imagery", e) + } + } + } + + override fun onCacheOverlay(cacheOverlays: List) { + activity?.runOnUiThread { + val layers = layerLocalDataSource.readByEvent(event, "Imagery") + + adapter.clear() + adapter.notifyDataSetChanged() + val secureLayers = mutableListOf() + val insecureLayers = mutableListOf() + + // Set what should be checked based on preferences. + val overlays = preferences.getStringSet(resources.getString(R.string.onlineLayersKey), emptySet()) + for (layer in layers) { + val enabled = overlays?.contains(layer.name) ?: false + cacheProvider.getOverlay(layer.name)?.let { overlay -> + overlay.isEnabled = enabled + } + + if (URLUtil.isHttpsUrl(layer.url)) { + secureLayers.add(layer) + } else { + insecureLayers.add(layer) + } + } + val compare = java.util.Comparator { o1, o2 -> o1.name.compareTo(o2.name) } + Collections.sort(secureLayers, compare) + Collections.sort(insecureLayers, compare) + adapter.addAllNonLayers(insecureLayers) + adapter.addAllSecureLayers(secureLayers) + adapter.notifyDataSetChanged() + if (layers.isNotEmpty()) { + noContentView.visibility = View.GONE + contentView.visibility = View.VISIBLE + } else { + contentView.visibility = View.GONE + noContentView.visibility = View.VISIBLE + } + refreshButton.isEnabled = true + swipeContainer.isRefreshing = false + } + } + + val selectedOverlays: ArrayList + get() { + val overlays = ArrayList() + cacheProvider.cacheOverlays.let { overlay -> + if (overlay is URLCacheOverlay) { + if (overlay.isEnabled) { + overlays.add(overlay.name) + } + } + } + + return overlays + } + + override fun onDestroy() { + super.onDestroy() + cacheProvider.unregisterCacheOverlayListener(this) + } + + override fun onResume() { + super.onResume() + recyclerView.adapter = adapter + if (listState != null) { + recyclerView.layoutManager!!.onRestoreInstanceState(listState) + } + } + + override fun onPause() { + super.onPause() + listState = recyclerView.layoutManager!!.onSaveInstanceState() + recyclerView.adapter = null + } + } + + /** + * ALL public methods MUST be made on the UI thread to ensure concurrency.** + */ + class OnlineLayersAdapter internal constructor( + private val context: Context, + private val cacheProvider: CacheProvider + ): RecyclerView.Adapter() { + private val secureLayers = mutableListOf() + private val nonSecureLayers = mutableListOf() + + fun clear() { + secureLayers.clear() + nonSecureLayers.clear() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return if (viewType == ITEM_TYPE_LAYER) { + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.online_layers_list_item, parent, false) + LayerViewHolder(itemView) + } else { + val itemView = LayoutInflater.from(parent.context).inflate(R.layout.event_list_section_header, parent, false) + SectionViewHolder(itemView) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, i: Int) { + if (holder is LayerViewHolder) { + bindLayerViewHolder(holder, i) + } else if (holder is SectionViewHolder) { + bindSectionViewHolder(holder, i) + } + } + + private fun bindLayerViewHolder(holder: LayerViewHolder, i: Int) { + val view = holder.itemView + val layer = if (i <= secureLayers.size) { + secureLayers[i - 1] + } else { + nonSecureLayers[i - secureLayers.size - 2] + } + val title = view.findViewById(R.id.online_layers_title) + title.text = layer.name + val summary = view.findViewById(R.id.online_layers_summary) + summary.text = layer.url + val toggle = view.findViewById(R.id.online_layers_toggle) + if (URLUtil.isHttpUrl(layer.url)) { + view.setOnClickListener { + AlertDialog.Builder(context) + .setTitle("Non HTTPS Layer") + .setMessage("We cannot load this layer on mobile because it cannot be accessed securely.") + .setPositiveButton("OK", null).show() + } + toggle.setOnClickListener(null) + toggle.isEnabled = false + (toggle as Checkable).isChecked = false + } else { + view.setOnClickListener { toggle.performClick() } + toggle.setOnClickListener { v -> + val isChecked = (v as Checkable).isChecked + cacheProvider.getOverlay(layer.name)?.let { overlay -> + overlay.isEnabled = isChecked + } + } + toggle.isEnabled = true + cacheProvider.getOverlay(layer.name)?.let { overlay -> + (toggle as Checkable).isChecked = overlay.isEnabled + } + } + } + + private fun bindSectionViewHolder(holder: SectionViewHolder, position: Int) { + holder.sectionName.text = if (position == 0) "Secure Layers" else "Nonsecure Layers" + } + + fun addAllSecureLayers(layers: List?) { + secureLayers.addAll(layers!!) + } + + fun addAllNonLayers(layers: List?) { + nonSecureLayers.addAll(layers!!) + } + + override fun getItemCount(): Int { + return secureLayers.size + nonSecureLayers.size + 2 + } + + override fun getItemViewType(position: Int): Int { + return if (position == 0 || position == secureLayers.size + 1) { + ITEM_TYPE_HEADER + } else { + ITEM_TYPE_LAYER + } + } + + private inner class LayerViewHolder(view: View?) : RecyclerView.ViewHolder(view!!) + private inner class SectionViewHolder(view: View) : + RecyclerView.ViewHolder(view) { + val sectionName: TextView + + init { + sectionName = view.findViewById(R.id.section_name) + } + } + + companion object { + private const val ITEM_TYPE_HEADER = 1 + private const val ITEM_TYPE_LAYER = 2 + } + } + + companion object { + private val LOG_NAME = OnlineLayersPreferenceActivity::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.java b/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.java deleted file mode 100644 index 355595b99..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.java +++ /dev/null @@ -1,777 +0,0 @@ -package mil.nga.giat.mage.map.preference; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Environment; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ExpandableListView; - -import androidx.annotation.MainThread; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import androidx.fragment.app.ListFragment; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; - -import mil.nga.geopackage.GeoPackageFactory; -import mil.nga.geopackage.GeoPackageManager; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.cache.CacheUtils; -import mil.nga.giat.mage.map.cache.CacheOverlay; -import mil.nga.giat.mage.map.cache.CacheOverlayFilter; -import mil.nga.giat.mage.map.cache.CacheProvider; -import mil.nga.giat.mage.map.cache.CacheProvider.OnCacheOverlayListener; -import mil.nga.giat.mage.map.cache.GeoPackageCacheOverlay; -import mil.nga.giat.mage.map.cache.StaticFeatureCacheOverlay; -import mil.nga.giat.mage.map.cache.XYZDirectoryCacheOverlay; -import mil.nga.giat.mage.map.download.GeoPackageDownloadManager; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.exceptions.LayerException; -import mil.nga.giat.mage.sdk.fetch.StaticFeatureServerFetch; -import mil.nga.giat.mage.sdk.gson.deserializer.LayerDeserializer; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import mil.nga.giat.mage.sdk.http.resource.LayerResource; -import mil.nga.giat.mage.sdk.utils.StorageUtility; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; - -/** - * This activity is the offline layers section and deals with geopackages and static features - */ -public class TileOverlayPreferenceActivity extends AppCompatActivity { - - private static final String LOG_NAME = TileOverlayPreferenceActivity.class.getName(); - - private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 100; - - private OverlayListFragment offlineLayersFragment; - - private static volatile SharedPreferences ourSharedPreferences; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_offline_layers); - - offlineLayersFragment = (OverlayListFragment) getSupportFragmentManager().findFragmentById(R.id.offline_layers_fragment); - ourSharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - } - - @Override - public void onBackPressed() { - SharedPreferences.Editor editor = ourSharedPreferences.edit(); - editor.putStringSet(getResources().getString(R.string.tileOverlaysKey), new HashSet<>(offlineLayersFragment.getSelectedOverlays())); - editor.commit(); - - synchronized (offlineLayersFragment.timerLock) { - if (this.offlineLayersFragment.downloadRefreshTimer != null) { - this.offlineLayersFragment.downloadRefreshTimer.cancel(); - } - } - - finish(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - public static class OverlayListFragment extends ListFragment implements OnCacheOverlayListener { - - private OfflineLayersAdapter adapter; - private final Object adapterLock = new Object(); - private ExpandableListView listView; - private View contentView; - private View noContentView; - private MenuItem refreshButton; - private SwipeRefreshLayout swipeContainer; - private GeoPackageDownloadManager downloadManager; - private Timer downloadRefreshTimer; - private final Object timerLock = new Object(); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - - downloadManager = new GeoPackageDownloadManager(getActivity().getApplicationContext(), new GeoPackageDownloadManager.GeoPackageDownloadListener() { - @Override - public void onGeoPackageDownloaded(final Layer layer, final CacheOverlay overlay) { - - getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - synchronized (adapterLock) { - adapter.addOverlay(overlay, layer); - adapter.notifyDataSetChanged(); - } - } - }); - } - }); - - adapter = new OfflineLayersAdapter(getActivity(), downloadManager); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_offline_layers, container, false); - listView = view.findViewById(android.R.id.list); - listView.setEnabled(true); - listView.setAdapter(adapter); - - listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { - int itemType = ExpandableListView.getPackedPositionType(id); - if (itemType == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { - // TODO Handle child row long clicks here - return true; - } else if (itemType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { - int groupPosition = ExpandableListView.getPackedPositionGroup(id); - - if(groupPosition != -1) { - synchronized (adapterLock) { - Object group = adapter.getGroup(groupPosition); - if (group instanceof CacheOverlay) { - CacheOverlay cacheOverlay = (CacheOverlay) adapter.getGroup(groupPosition); - deleteCacheOverlayConfirm(cacheOverlay); - return true; - } - } - } - - Log.w(LOG_NAME, "Failed to locate group at index " + id); - return false; - } - - return false; - } - }); - - swipeContainer = view.findViewById(R.id.offline_layers_swipeContainer); - swipeContainer.setColorSchemeResources(R.color.md_blue_600, R.color.md_orange_A200); - swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { - @Override - public void onRefresh() { - softRefresh(refreshButton); - hardRefresh(); - } - }); - - contentView = view.findViewById(R.id.downloadable_layers_content); - noContentView = view.findViewById(R.id.downloadable_layers_no_content); - noContentView.setVisibility(View.GONE); - - if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE)) { - new AlertDialog.Builder(getActivity(), R.style.AppCompatAlertDialogStyle) - .setTitle(R.string.offline_layers_access_title) - .setMessage(R.string.offline_layers_access_message) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); - } - }) - .create() - .show(); - - } else { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE); - } - } - - return view; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - CacheProvider.getInstance(getActivity()).unregisterCacheOverlayListener(this); - } - - @Override - public void onResume() { - super.onResume(); - - downloadManager.onResume(); - - synchronized (timerLock) { - downloadRefreshTimer = new Timer(); - downloadRefreshTimer.schedule(new GeopackageDownloadProgressTimer(getActivity()), 0, 2000); - } - } - - @Override - public void onPause() { - super.onPause(); - - CacheProvider.getInstance(getActivity()).unregisterCacheOverlayListener(this); - - downloadManager.onPause(); - - synchronized (timerLock) { - if (downloadRefreshTimer != null) { - downloadRefreshTimer.cancel(); - } - } - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.offline_layers_menu, menu); - - refreshButton = menu.findItem(R.id.tile_overlay_refresh); - refreshButton.setEnabled(true); - - CacheProvider.getInstance(getActivity()).registerCacheOverlayListener(this, false); - softRefresh(refreshButton); - refreshLocalDownloadableLayers(); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.tile_overlay_refresh: - softRefresh(item); - hardRefresh(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - @MainThread - private void softRefresh(MenuItem item){ - item.setEnabled(false); - - synchronized (adapterLock) { - adapter.getDownloadableLayers().clear(); - adapter.getOverlays().clear(); - adapter.getSideloadedOverlays().clear(); - adapter.notifyDataSetChanged(); - } - - contentView.setVisibility(View.GONE); - noContentView.setVisibility(View.VISIBLE); - listView.setEnabled(false); - swipeContainer.setRefreshing(true); - } - - /** - * Attempt to pull all the layers from the remote server as well as refreshing any local overlays - * - */ - private void hardRefresh() { - - @SuppressLint("StaticFieldLeak") AsyncTask fetcher = new AsyncTask() { - @Override - protected Void doInBackground(Void... objects) { - fetchRemoteGeopackageLayers(); - fetchRemoteStaticLayers(); - - return null; - } - - @Override - protected void onPostExecute(Void v) { - super.onPostExecute(v); - - refreshLocalDownloadableLayers(); - } - }; - fetcher.execute(); - } - - private void refreshLocalDownloadableLayers() { - @SuppressLint("StaticFieldLeak") AsyncTask> fetcher = - new AsyncTask>() { - @Override - protected List doInBackground(Void... objects) { - final Event event = - EventHelper.getInstance(getActivity().getApplicationContext()).getCurrentEvent(); - - List layers = new ArrayList<>(); - try { - for (Layer layer : LayerHelper.getInstance(getActivity().getApplicationContext()).readByEvent(event, null)) { - if (layer.getType().equalsIgnoreCase("GeoPackage") - || layer.getType().equalsIgnoreCase("Feature")) { - if (!layer.isLoaded() && layer.getDownloadId() == null) { - layers.add(layer); - } - } - } - } catch (LayerException e) { - Log.e(LOG_NAME, "Error refreshing local downloadable layers",e); - } - - return layers; - } - - @Override - protected void onPostExecute(List layers) { - super.onPostExecute(layers); - - synchronized (adapterLock) { - adapter.getDownloadableLayers().addAll(layers); - Collections.sort(adapter.getDownloadableLayers(), new LayerNameComparator()); - //The adapter will be notified of a data set change in onCacheOverlay - CacheProvider.getInstance(getActivity().getApplicationContext()).refreshTileOverlays(); - } - } - }; - fetcher.execute(); - } - - /** - * This reads the remote layers from the server but does not download them - * - */ - private void fetchRemoteStaticLayers(){ - StaticFeatureServerFetch staticFeatureServerFetch = new StaticFeatureServerFetch(getContext()); - staticFeatureServerFetch.fetch(false, null); - } - - private void fetchRemoteGeopackageLayers() { - Context context = getContext(); - Event event = EventHelper.getInstance(context).getCurrentEvent(); - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(LayerDeserializer.getGsonBuilder())) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LayerResource.LayerService service = retrofit.create(LayerResource.LayerService.class); - - try { - Response> response = - service.getLayers(event.getRemoteId(), "GeoPackage").execute(); - if (response.isSuccessful()) { - saveGeopackageLayers(response.body(), event); - } - }catch (IOException e){ - Log.w(LOG_NAME, "Failed to fect geopackages",e); - } - } - - private void saveGeopackageLayers(Collection remoteLayers, Event event) { - Context context = getActivity().getApplicationContext(); - LayerHelper layerHelper = LayerHelper.getInstance(context); - try { - // get local layers - Collection localLayers = layerHelper.readAll("GeoPackage"); - - Map remoteIdToLayer = new HashMap<>(localLayers.size()); - for(Layer layer : localLayers){ - remoteIdToLayer.put(layer.getRemoteId(), layer); - } - - GeoPackageManager manager = GeoPackageFactory.getManager(context); - for (Layer remoteLayer : remoteLayers) { - remoteLayer.setEvent(event); - // Check if its loaded - File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), - String.format("MAGE/geopackages/%s/%s", remoteLayer.getRemoteId(), remoteLayer.getFileName())); - if (file.exists() && manager.existsAtExternalFile(file)) { - remoteLayer.setLoaded(true); - } - if(!localLayers.contains(remoteLayer)) { - layerHelper.create(remoteLayer); - }else { - Layer localLayer = remoteIdToLayer.get(remoteLayer.getRemoteId()); - //Only remove a local layer if the even has changed - if (!remoteLayer.getEvent().equals(localLayer.getEvent())) { - layerHelper.delete(localLayer.getId()); - layerHelper.create(remoteLayer); - } - } - } - } catch (LayerException e) { - Log.e(LOG_NAME, "Error saving geopackage layers", e); - } - } - - @Override - @MainThread - public void onCacheOverlay(final List cacheOverlays) { - final Event event = EventHelper.getInstance(getActivity().getApplicationContext()).getCurrentEvent(); - - List geopackages = Collections.EMPTY_LIST; - try { - geopackages = LayerHelper.getInstance(getActivity().getApplicationContext()).readByEvent(event, "GeoPackage"); - } catch (LayerException e) { - Log.w(LOG_NAME, "Error reading geopackage layers",e); - } - - downloadManager.reconcileDownloads(geopackages, - new GeoPackageDownloadManager.GeoPackageLoadListener() { - - @MainThread - @Override - public void onReady(List layers) { - - boolean isEmpty = false; - synchronized (adapterLock){ - adapter.getDownloadableLayers().removeAll(layers); - adapter.getDownloadableLayers().addAll(layers); - - adapter.getOverlays().clear(); - adapter.getSideloadedOverlays().clear(); - - List filtered = - new CacheOverlayFilter(getContext(), event).filter(cacheOverlays); - for(CacheOverlay overlay : filtered) { - if (overlay instanceof GeoPackageCacheOverlay) { - if (overlay.isSideloaded()) { - adapter.getSideloadedOverlays().add(overlay); - } else { - adapter.getOverlays().add(overlay); - } - } else if (overlay instanceof StaticFeatureCacheOverlay) { - adapter.getOverlays().add(overlay); - } - } - - Collections.sort(adapter.getDownloadableLayers(), new LayerNameComparator()); - Collections.sort(adapter.getSideloadedOverlays()); - Collections.sort(adapter.getOverlays()); - - if(adapter.getDownloadableLayers().isEmpty() - && adapter.getOverlays().isEmpty() - && adapter.getSideloadedOverlays().isEmpty()) { - isEmpty = true; - } - - refreshButton.setEnabled(true); - if (!isEmpty) { - noContentView.setVisibility(View.GONE); - contentView.setVisibility(View.VISIBLE); - } else { - contentView.setVisibility(View.GONE); - noContentView.setVisibility(View.VISIBLE); - } - swipeContainer.setRefreshing(false); - listView.setEnabled(true); - - adapter.notifyDataSetChanged(); - } - } - }); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - switch (requestCode) { - case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: { - if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - CacheProvider.getInstance(getActivity()).refreshTileOverlays(); - } - - break; - } - } - } - - /** - * Get the selected cache overlays and child cache overlays - * - * @return added cache overlays - */ - public ArrayList getSelectedOverlays() { - ArrayList overlays = new ArrayList<>(); - for (CacheOverlay cacheOverlay : CacheProvider.getInstance(getContext()).getCacheOverlays()) { - if(cacheOverlay instanceof GeoPackageCacheOverlay) { - boolean childAdded = false; - for (CacheOverlay childCache : cacheOverlay.getChildren()) { - if (childCache.isEnabled()) { - overlays.add(childCache.getCacheName()); - childAdded = true; - } - } - - if (!childAdded && cacheOverlay.isEnabled()) { - overlays.add(cacheOverlay.getCacheName()); - } - } else if(cacheOverlay.isEnabled()) { - if(cacheOverlay instanceof StaticFeatureCacheOverlay || - cacheOverlay instanceof XYZDirectoryCacheOverlay) { - overlays.add(cacheOverlay.getCacheName()); - } - } - } - - return overlays; - } - - /** - * Delete the cache overlay - * @param cacheOverlay - */ - @MainThread - private void deleteCacheOverlayConfirm(final CacheOverlay cacheOverlay) { - AlertDialog deleteDialog = new AlertDialog.Builder(getActivity()) - .setTitle("Delete Layer") - .setMessage("Delete " + cacheOverlay.getName() + " Layer?") - .setPositiveButton("Delete", - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - deleteCacheOverlay(cacheOverlay); - } - }) - - .setNegativeButton(getString(R.string.cancel), - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, - int which) { - dialog.dismiss(); - } - }).create(); - deleteDialog.show(); - } - - /** - * Delete the XYZ cache overlay - * @param xyzCacheOverlay - */ - private void deleteXYZCacheOverlay(XYZDirectoryCacheOverlay xyzCacheOverlay){ - - File directory = xyzCacheOverlay.getDirectory(); - - if(directory.canWrite()){ - deleteFile(directory); - } - - } - - /** - * Delete the base directory file - * @param base directory - */ - private void deleteFile(File base) { - if (base.isDirectory()) { - for (File file : base.listFiles()) { - deleteFile(file); - } - } - base.delete(); - } - - /** - * Delete the cache overlay - * @param cacheOverlay - */ - @MainThread - private void deleteCacheOverlay(final CacheOverlay cacheOverlay){ - - softRefresh(refreshButton); - - @SuppressLint("StaticFieldLeak") AsyncTask deleteTask = new AsyncTask() { - @Override - protected Void doInBackground(Void... voids) { - switch(cacheOverlay.getType()) { - - case XYZ_DIRECTORY: - deleteXYZCacheOverlay((XYZDirectoryCacheOverlay)cacheOverlay); - break; - - case GEOPACKAGE: - deleteGeoPackageCacheOverlay((GeoPackageCacheOverlay)cacheOverlay); - break; - - case STATIC_FEATURE: - deleteStaticFeatureCacheOverlay((StaticFeatureCacheOverlay)cacheOverlay); - break; - } - - hardRefresh(); - - return null; - } - }; - - deleteTask.execute(); - } - - /** - * Delete the GeoPackage cache overlay - * @param geoPackageCacheOverlay - */ - private void deleteGeoPackageCacheOverlay(GeoPackageCacheOverlay geoPackageCacheOverlay) { - - String database = geoPackageCacheOverlay.getName(); - - // Get the GeoPackage file - GeoPackageManager manager = GeoPackageFactory.getManager(getActivity()); - File path = manager.getFile(database); - - // Delete the cache from the GeoPackage manager - manager.delete(database); - - // Attempt to delete the cache file if it is in the cache directory - File pathDirectory = path.getParentFile(); - if(path.canWrite() && pathDirectory != null) { - Map storageLocations = StorageUtility.getWritableStorageLocations(); - for (File storageLocation : storageLocations.values()) { - File root = new File(storageLocation, getString(R.string.overlay_cache_directory)); - if (root.equals(pathDirectory)) { - path.delete(); - break; - } - } - } - - // Check internal/external application storage - File applicationCacheDirectory = CacheUtils.getApplicationCacheDirectory(getActivity()); - if (applicationCacheDirectory != null && applicationCacheDirectory.exists()) { - for (File cache : applicationCacheDirectory.listFiles()) { - if (cache.equals(path)) { - path.delete(); - break; - } - } - } - - if (path.getAbsolutePath().startsWith(String.format("%s/MAGE/geopackages", getContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)))) { - LayerHelper layerHelper = LayerHelper.getInstance(getActivity().getApplicationContext()); - - try { - String relativePath = path.getAbsolutePath().split(getContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + "/")[1]; - Layer layer = layerHelper.getByRelativePath(relativePath); - if (layer != null) { - layer.setLoaded(false); - layer.setDownloadId(null); - layerHelper.update(layer); - } - } catch (LayerException e) { - Log.e(LOG_NAME, "Error setting loaded to false for path " + path, e); - } - - if (!path.delete()) { - Log.e(LOG_NAME, "Error deleting geopackage file from filesystem for path " + path); - } - } - } - - private void deleteStaticFeatureCacheOverlay(StaticFeatureCacheOverlay cacheOverlay) { - try { - LayerHelper.getInstance(getContext()).delete(cacheOverlay.getId()); - } catch (LayerException e) { - Log.w(LOG_NAME, "Failed to delete static feature " + cacheOverlay.getCacheName() ,e); - } - } - - private class GeopackageDownloadProgressTimer extends TimerTask { - private boolean canceled = false; - - private final Activity myActivity; - - public GeopackageDownloadProgressTimer(Activity activity){ - myActivity = activity; - } - - @Override - public void run() { - synchronized (timerLock) { - if (!canceled) { - try { - updateGeopackageDownloadProgress(); - }catch(Exception e){ - //ignore - } - } - } - } - - @Override - public boolean cancel() { - synchronized (timerLock) { - canceled = true; - } - return super.cancel(); - } - - private void updateGeopackageDownloadProgress() { - myActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - synchronized (adapterLock) { - try { - List layers = adapter.getDownloadableLayers(); - for(Layer layer : layers){ - synchronized (timerLock){ - if(canceled){ - return; - } - } - if(layer.getDownloadId() == null || layer.isLoaded()){ - continue; - } - - for(int i = 0; i < layers.size(); i++) { - final View view = listView.getChildAt(i); - if (view == null || view.getTag() == null || !view.getTag().equals(layer.getName())) { - continue; - } - - adapter.updateDownloadProgress(view, layer); - } - } - } catch (Exception e) { - //ignore - } - } - } - }); - } - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.kt b/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.kt new file mode 100644 index 000000000..1702517c0 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/map/preference/TileOverlayPreferenceActivity.kt @@ -0,0 +1,597 @@ +package mil.nga.giat.mage.map.preference + +import android.app.Activity +import android.content.SharedPreferences +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView.OnItemLongClickListener +import android.widget.ExpandableListView +import androidx.annotation.MainThread +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.ListFragment +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.geopackage.GeoPackageFactory +import mil.nga.giat.mage.R +import mil.nga.giat.mage.cache.CacheUtils +import mil.nga.giat.mage.data.repository.layer.LayerRepository +import mil.nga.giat.mage.map.cache.CacheOverlay +import mil.nga.giat.mage.map.cache.CacheOverlayFilter +import mil.nga.giat.mage.map.cache.CacheOverlayType +import mil.nga.giat.mage.map.cache.CacheProvider +import mil.nga.giat.mage.map.cache.CacheProvider.OnCacheOverlayListener +import mil.nga.giat.mage.map.cache.GeoPackageCacheOverlay +import mil.nga.giat.mage.map.cache.StaticFeatureCacheOverlay +import mil.nga.giat.mage.map.cache.XYZDirectoryCacheOverlay +import mil.nga.giat.mage.map.download.GeoPackageDownloadManager +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.data.datasource.layer.LayerLocalDataSource +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.sdk.exceptions.LayerException +import mil.nga.giat.mage.sdk.utils.StorageUtility +import java.io.File +import java.util.Collections +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject + +@AndroidEntryPoint +class TileOverlayPreferenceActivity : AppCompatActivity() { + @Inject lateinit var preferences: SharedPreferences + + private lateinit var offlineLayersFragment: OverlayListFragment + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_offline_layers) + offlineLayersFragment = supportFragmentManager.findFragmentById(R.id.offline_layers_fragment) as OverlayListFragment + } + + override fun onBackPressed() { + val editor = preferences.edit() + editor.putStringSet(resources.getString(R.string.tileOverlaysKey), HashSet(offlineLayersFragment.getSelectedOverlays())) + editor.apply() + synchronized(offlineLayersFragment.timerLock) { + if (offlineLayersFragment.downloadRefreshTimer != null) { + offlineLayersFragment.downloadRefreshTimer!!.cancel() + } + } + finish() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + @AndroidEntryPoint + class OverlayListFragment : ListFragment(), OnCacheOverlayListener { + + @Inject lateinit var cacheProvider: CacheProvider + @Inject lateinit var layerRepository: LayerRepository + @Inject lateinit var layerLocalDataSource: LayerLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + + private lateinit var adapter: OfflineLayersAdapter + private val adapterLock = Any() + private lateinit var listView: ExpandableListView + private lateinit var contentView: View + private lateinit var noContentView: View + private lateinit var refreshButton: MenuItem + private lateinit var swipeContainer: SwipeRefreshLayout + private lateinit var downloadManager: GeoPackageDownloadManager + var downloadRefreshTimer: Timer? = null + val timerLock = Any() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setHasOptionsMenu(true) + downloadManager = + GeoPackageDownloadManager( + requireActivity().applicationContext, + cacheProvider, + layerLocalDataSource + ) { layer: Layer, overlay: CacheOverlay -> + activity?.runOnUiThread { + synchronized(adapterLock) { + adapter.addOverlay(overlay, layer) + adapter.notifyDataSetChanged() + } + } + } + val event = eventLocalDataSource.currentEvent + adapter = OfflineLayersAdapter( + requireActivity(), + cacheProvider, + downloadManager, + layerRepository, + layerLocalDataSource, + event + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_offline_layers, container, false) + listView = view.findViewById(android.R.id.list) + listView.isEnabled = true + listView.setAdapter(adapter) + listView.onItemLongClickListener = OnItemLongClickListener { _, _, _, id -> + val itemType = ExpandableListView.getPackedPositionType(id) + if (itemType == ExpandableListView.PACKED_POSITION_TYPE_CHILD) { + // TODO Handle child row long clicks here + return@OnItemLongClickListener true + } else if (itemType == ExpandableListView.PACKED_POSITION_TYPE_GROUP) { + val groupPosition = ExpandableListView.getPackedPositionGroup(id) + if (groupPosition != -1) { + synchronized(adapterLock) { + val group = adapter.getGroup(groupPosition) + if (group is CacheOverlay) { + val cacheOverlay = adapter.getGroup(groupPosition) as CacheOverlay + deleteCacheOverlayConfirm(cacheOverlay) + return@OnItemLongClickListener true + } + } + } + Log.w(LOG_NAME, "Failed to locate group at index $id") + return@OnItemLongClickListener false + } + false + } + swipeContainer = view.findViewById(R.id.offline_layers_swipeContainer) + swipeContainer.setColorSchemeResources(R.color.md_blue_600, R.color.md_orange_A200) + swipeContainer.setOnRefreshListener { + softRefresh(refreshButton) + hardRefresh() + } + contentView = view.findViewById(R.id.downloadable_layers_content) + noContentView = view.findViewById(R.id.downloadable_layers_no_content) + noContentView.visibility = View.GONE + + CoroutineScope(Dispatchers.IO).launch { + cacheProvider.refreshTileOverlays() + } + + return view + } + + override fun onDestroy() { + super.onDestroy() + cacheProvider.unregisterCacheOverlayListener(this) + } + + override fun onResume() { + super.onResume() + downloadManager.onResume() + synchronized(timerLock) { + downloadRefreshTimer = Timer() + downloadRefreshTimer?.schedule(GeopackageDownloadProgressTimer(activity), 0, 2000) + } + } + + override fun onPause() { + super.onPause() + cacheProvider.unregisterCacheOverlayListener(this) + downloadManager.onPause() + synchronized(timerLock) { + if (downloadRefreshTimer != null) { + downloadRefreshTimer?.cancel() + } + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.offline_layers_menu, menu) + refreshButton = menu.findItem(R.id.tile_overlay_refresh) + refreshButton.isEnabled = true + cacheProvider.registerCacheOverlayListener(this, false) + softRefresh(refreshButton) + refreshLocalDownloadableLayers() + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.tile_overlay_refresh) { + softRefresh(item) + hardRefresh() + return true + } + return super.onOptionsItemSelected(item) + } + + @MainThread + private fun softRefresh(item: MenuItem?) { + item!!.isEnabled = false + synchronized(adapterLock) { + adapter.downloadableLayers.clear() + adapter.overlays.clear() + adapter.sideloadedOverlays.clear() + adapter.notifyDataSetChanged() + } + contentView.visibility = View.GONE + noContentView.visibility = View.VISIBLE + listView.isEnabled = false + swipeContainer.isRefreshing = true + } + + /** + * Attempt to pull all the layers from the remote server as well as refreshing any local overlays + * + */ + private fun hardRefresh() { + CoroutineScope(Dispatchers.IO).launch { + fetchRemoteGeopackageLayers() + fetchRemoteStaticLayers() + refreshLocalDownloadableLayers() + } + } + + private fun refreshLocalDownloadableLayers() { + CoroutineScope(Dispatchers.IO).launch { + val event = eventLocalDataSource.currentEvent + val layers: MutableList = ArrayList() + for (layer in layerLocalDataSource.readByEvent(event, null)) { + if (layer.type.equals("GeoPackage", ignoreCase = true) || + layer.type.equals("Feature", ignoreCase = true) + ) { + if (!layer.isLoaded && layer.downloadId == null) { + layers.add(layer) + } + } + } + + synchronized(adapterLock) { + adapter.downloadableLayers.addAll(layers) + Collections.sort(adapter.downloadableLayers, LayerNameComparator()) + // The adapter will be notified of a data set change in onCacheOverlay + + CoroutineScope(Dispatchers.IO).launch { + cacheProvider.refreshTileOverlays() + } + } + } + } + + /** + * This reads the remote layers from the server but does not download them + * + */ + private fun fetchRemoteStaticLayers() { + CoroutineScope(Dispatchers.IO).launch { + eventLocalDataSource.currentEvent?.let { event -> + layerRepository.fetchFeatureLayers(event, false) + } + } + } + + private fun fetchRemoteGeopackageLayers() { + CoroutineScope(Dispatchers.IO).launch { + eventLocalDataSource.currentEvent?.let { event -> + val layers = layerRepository.fetchLayers(event, "GeoPackage") + saveGeopackageLayers(layers, event) + } + } + } + + private fun saveGeopackageLayers(remoteLayers: Collection, event: Event) { + val context = requireActivity().applicationContext + try { + // get local layers + val localLayers: Collection = layerLocalDataSource.readAll("GeoPackage") + val remoteIdToLayer: MutableMap = HashMap(localLayers.size) + for (layer in localLayers) { + remoteIdToLayer[layer.remoteId] = layer + } + val manager = GeoPackageFactory.getManager(context) + for (remoteLayer in remoteLayers) { + remoteLayer.event = event + // Check if its loaded + val file = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + String.format( + "MAGE/geopackages/%s/%s", + remoteLayer.remoteId, + remoteLayer.fileName + ) + ) + if (file.exists() && manager.existsAtExternalFile(file)) { + remoteLayer.isLoaded = true + } + if (!localLayers.contains(remoteLayer)) { + layerLocalDataSource.create(remoteLayer) + } else { + val localLayer = remoteIdToLayer[remoteLayer.remoteId] + //Only remove a local layer if the even has changed + if (remoteLayer.event != localLayer!!.event) { + layerLocalDataSource.delete(localLayer.id) + layerLocalDataSource.create(remoteLayer) + } + } + } + } catch (e: LayerException) { + Log.e(LOG_NAME, "Error saving geopackage layers", e) + } + } + + @MainThread + override fun onCacheOverlay(cacheOverlays: List) { + val event = eventLocalDataSource.currentEvent + val geopackages = layerLocalDataSource.readByEvent(event, "GeoPackage") + + downloadManager.reconcileDownloads( + geopackages + ) { layers -> + var isEmpty = false + synchronized(adapterLock) { + adapter.downloadableLayers.removeAll(layers) + adapter.downloadableLayers.addAll(layers) + adapter.overlays.clear() + adapter.sideloadedOverlays.clear() + + val filtered = CacheOverlayFilter( + context = requireContext().applicationContext, + layers = layerLocalDataSource.readByEvent(event, "GeoPackage") + ).filter(cacheOverlays) + + filtered.forEach { overlay -> + if (overlay is GeoPackageCacheOverlay) { + if (overlay.isSideloaded()) { + adapter.sideloadedOverlays.add(overlay) + } else { + adapter.overlays.add(overlay) + } + } else if (overlay is StaticFeatureCacheOverlay) { + adapter.overlays.add(overlay) + } + } + + Collections.sort(adapter.downloadableLayers, LayerNameComparator()) + adapter.sideloadedOverlays.sort() + adapter.overlays.sort() + if (adapter.downloadableLayers.isEmpty() + && adapter.overlays.isEmpty() + && adapter.sideloadedOverlays.isEmpty() + ) { + isEmpty = true + } + refreshButton.isEnabled = true + if (!isEmpty) { + noContentView.visibility = View.GONE + contentView.visibility = View.VISIBLE + } else { + contentView.visibility = View.GONE + noContentView.visibility = View.VISIBLE + } + swipeContainer.isRefreshing = false + listView.isEnabled = true + adapter.notifyDataSetChanged() + } + } + } + + /** + * Get the selected cache overlays and child cache overlays + * + * @return added cache overlays + */ + fun getSelectedOverlays(): ArrayList { + val overlays = ArrayList() + cacheProvider.cacheOverlays.forEach { (_, overlay) -> + if (overlay is GeoPackageCacheOverlay) { + var childAdded = false + for (childCache in overlay.children) { + if (childCache.isEnabled) { + overlays.add(childCache.cacheName) + childAdded = true + } + } + if (!childAdded && overlay.isEnabled) { + overlays.add(overlay.cacheName) + } + } else if (overlay.isEnabled) { + if (overlay is StaticFeatureCacheOverlay || + overlay is XYZDirectoryCacheOverlay + ) { + overlays.add(overlay.cacheName) + } + } + } + + return overlays + } + + /** + * Delete the cache overlay + * @param cacheOverlay + */ + @MainThread + private fun deleteCacheOverlayConfirm(cacheOverlay: CacheOverlay) { + val deleteDialog = AlertDialog.Builder(requireActivity()) + .setTitle("Delete Layer") + .setMessage("Delete " + cacheOverlay.name + " Layer?") + .setPositiveButton( + "Delete" + ) { _, _ -> deleteCacheOverlay(cacheOverlay) } + .setNegativeButton( + getString(R.string.cancel) + ) { dialog, _ -> dialog.dismiss() }.create() + deleteDialog.show() + } + + /** + * Delete the XYZ cache overlay + * @param xyzCacheOverlay + */ + private fun deleteXYZCacheOverlay(xyzCacheOverlay: XYZDirectoryCacheOverlay) { + val directory = xyzCacheOverlay.directory + if (directory.canWrite()) { + deleteFile(directory) + } + } + + /** + * Delete the base directory file + * @param base directory + */ + private fun deleteFile(base: File) { + if (base.isDirectory) { + base.listFiles()?.forEach { file -> + deleteFile(file) + } + } + base.delete() + } + + /** + * Delete the cache overlay + * @param cacheOverlay + */ + private fun deleteCacheOverlay(cacheOverlay: CacheOverlay) { + CoroutineScope(Dispatchers.IO).launch { + when (cacheOverlay.type) { + CacheOverlayType.XYZ_DIRECTORY -> deleteXYZCacheOverlay(cacheOverlay as XYZDirectoryCacheOverlay) + CacheOverlayType.GEOPACKAGE -> deleteGeoPackageCacheOverlay(cacheOverlay as GeoPackageCacheOverlay) + CacheOverlayType.STATIC_FEATURE -> deleteStaticFeatureCacheOverlay(cacheOverlay as StaticFeatureCacheOverlay) + else -> {} + } + hardRefresh() + } + } + + /** + * Delete the GeoPackage cache overlay + * @param geoPackageCacheOverlay + */ + private fun deleteGeoPackageCacheOverlay(geoPackageCacheOverlay: GeoPackageCacheOverlay) { + val database = geoPackageCacheOverlay.name + + // Get the GeoPackage file + val manager = GeoPackageFactory.getManager(activity) + val path = manager.getFile(database) + + // Delete the cache from the GeoPackage manager + manager.delete(database) + + // Attempt to delete the cache file if it is in the cache directory + val pathDirectory = path.parentFile + if (path.canWrite() && pathDirectory != null) { + val storageLocations = StorageUtility.getWritableStorageLocations() + for (storageLocation in storageLocations.values) { + val root = File(storageLocation, getString(R.string.overlay_cache_directory)) + if (root == pathDirectory) { + path.delete() + break + } + } + } + + // Check internal/external application storage + val applicationCacheDirectory = CacheUtils.getApplicationCacheDirectory(requireContext().applicationContext) + if (applicationCacheDirectory.exists()) { + applicationCacheDirectory.listFiles() + ?.filter { it == path } + ?.forEach { it.delete() } + } + if (path.absolutePath.startsWith( + String.format( + "%s/MAGE/geopackages", + requireActivity().applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + ) + ) + ) { + try { + val relativePath = path.absolutePath.split( + (requireActivity().applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!.absolutePath + "/").toRegex() + ).dropLastWhile { it.isEmpty() }.toTypedArray()[1] + val layer = layerLocalDataSource.getByRelativePath(relativePath) + if (layer != null) { + layer.isLoaded = false + layer.downloadId = null + layerLocalDataSource.update(layer) + } + } catch (e: LayerException) { + Log.e(LOG_NAME, "Error setting loaded to false for path $path", e) + } + if (!path.delete()) { + Log.e(LOG_NAME, "Error deleting geopackage file from filesystem for path $path") + } + } + } + + private fun deleteStaticFeatureCacheOverlay(cacheOverlay: StaticFeatureCacheOverlay) { + try { + layerLocalDataSource.delete(cacheOverlay.id) + } catch (e: LayerException) { + Log.w(LOG_NAME, "Failed to delete static feature " + cacheOverlay.cacheName, e) + } + } + + private inner class GeopackageDownloadProgressTimer(private val myActivity: Activity?) : + TimerTask() { + private var canceled = false + override fun run() { + synchronized(timerLock) { + if (!canceled) { + try { + updateGeopackageDownloadProgress() + } catch (ignore: Exception) { } + } + } + } + + override fun cancel(): Boolean { + synchronized(timerLock) { canceled = true } + return super.cancel() + } + + private fun updateGeopackageDownloadProgress() { + myActivity!!.runOnUiThread(Runnable { + synchronized(adapterLock) { + try { + val layers = adapter.downloadableLayers + for (layer in layers) { + synchronized(timerLock) { + if (canceled) { + return@Runnable + } + } + if (layer.downloadId == null || layer.isLoaded) { + continue + } + for (i in layers.indices) { + val view = listView.getChildAt(i) + if (view == null || view.tag == null || view.tag != layer.name) { + continue + } + adapter.updateDownloadProgress(view, layer) + } + } + } catch (ignore: Exception) { } + } + }) + } + } + } + + companion object { + private val LOG_NAME = TileOverlayPreferenceActivity::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/ApiService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/ApiService.kt new file mode 100644 index 000000000..4f8d397ba --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/api/ApiService.kt @@ -0,0 +1,11 @@ +package mil.nga.giat.mage.network.api + +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Url + +interface ApiService { + @GET + suspend fun getApi(@Url url: String): Response +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/EventService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/EventService.kt deleted file mode 100644 index 17493184c..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/EventService.kt +++ /dev/null @@ -1,11 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.Team -import retrofit2.Response -import retrofit2.http.GET - -interface EventService { - @GET("/api/events") - suspend fun getEvents(): Response>> -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/LayerService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/LayerService.kt deleted file mode 100644 index 09d98f94f..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/LayerService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.layer.Layer -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query - -interface LayerService { - - @GET("/api/events/{eventId}/layers") - suspend fun getLayers(@Path("eventId") eventId: String?, @Query("type") type: String?): Response> - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/LocationService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/LocationService.kt deleted file mode 100644 index 041b5bc15..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/LocationService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.location.Location -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path - -interface LocationService { - - @GET("/api/events/{eventId}/locations/users") - suspend fun getLocations(@Path("eventId") eventId: String?): Response> - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/RoleService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/RoleService.kt deleted file mode 100644 index 69c6d8073..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/RoleService.kt +++ /dev/null @@ -1,10 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.user.Role -import retrofit2.Response -import retrofit2.http.GET - -interface RoleService { - @GET("/api/roles") - suspend fun getRoles(): Response> -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/TeamService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/TeamService.kt deleted file mode 100644 index 0112f005c..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/TeamService.kt +++ /dev/null @@ -1,14 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.user.Team -import mil.nga.giat.mage.sdk.datastore.user.User -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path - -interface TeamService { - - @GET("/api/events/{eventId}/teams?populate=users") - suspend fun getTeams(@Path("eventId") eventId: String?): Response>> - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/UserService.kt b/mage/src/main/java/mil/nga/giat/mage/network/api/UserService.kt deleted file mode 100644 index d7ba127ce..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/UserService.kt +++ /dev/null @@ -1,18 +0,0 @@ -package mil.nga.giat.mage.network.api - -import mil.nga.giat.mage.sdk.datastore.user.User -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Path - -interface UserService { - - @GET("/api/users/{userId}/icon") - suspend fun getIcon(@Path("userId") userId: String?): Response - - @POST("/api/users/{userId}/events/{eventId}/recent") - suspend fun addRecentEvent(@Path("userId") userId: String, @Path("eventId") eventId: String): Response - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/AttachmentService.kt b/mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentService.kt similarity index 93% rename from mage/src/main/java/mil/nga/giat/mage/network/api/AttachmentService.kt rename to mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentService.kt index b350bb060..6364f4044 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/AttachmentService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentService.kt @@ -1,6 +1,6 @@ -package mil.nga.giat.mage.network.api +package mil.nga.giat.mage.network.attachment -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import okhttp3.MultipartBody import okhttp3.ResponseBody import retrofit2.Response diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/AttachmentTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentTypeAdapter.kt similarity index 93% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/observation/AttachmentTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentTypeAdapter.kt index 9fb77569c..6c42f28cf 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/AttachmentTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/attachment/AttachmentTypeAdapter.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.network.gson.observation +package mil.nga.giat.mage.network.attachment import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader @@ -6,7 +6,7 @@ import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter import mil.nga.giat.mage.network.gson.nextLongOrNull import mil.nga.giat.mage.network.gson.nextStringOrNull -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment class AttachmentTypeAdapter: TypeAdapter() { override fun write(out: JsonWriter, value: Attachment) { diff --git a/mage/src/main/java/mil/nga/giat/mage/network/device/DeviceService.kt b/mage/src/main/java/mil/nga/giat/mage/network/device/DeviceService.kt new file mode 100644 index 000000000..2aacf5cc6 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/device/DeviceService.kt @@ -0,0 +1,16 @@ +package mil.nga.giat.mage.network.device + +import com.google.gson.JsonObject +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.Header +import retrofit2.http.POST + +interface DeviceService { + @POST("/auth/token") + suspend fun authorize( + @Header("Authorization") authorization: String?, + @Header("user-agent") userAgent: String?, + @Body body: JsonObject + ): Response +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/event/EventDeserializer.kt similarity index 90% rename from mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventDeserializer.kt rename to mage/src/main/java/mil/nga/giat/mage/network/event/EventDeserializer.kt index b7026b1e8..87d8ca7dc 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventDeserializer.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/event/EventDeserializer.kt @@ -1,10 +1,10 @@ -package mil.nga.giat.mage.sdk.gson.deserializer +package mil.nga.giat.mage.network.event import com.google.gson.* import mil.nga.giat.mage.network.gson.asJsonObjectOrNull import mil.nga.giat.mage.network.gson.asStringOrNull -import mil.nga.giat.mage.sdk.datastore.user.Event -import mil.nga.giat.mage.sdk.datastore.user.Form +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.event.Form import java.lang.reflect.Type class EventDeserializer : JsonDeserializer { @@ -21,7 +21,12 @@ class EventDeserializer : JsonDeserializer { val name = eventJson.get("name").asString val description = eventJson.get("description")?.asJsonPrimitive?.asString val acl = eventJson["acl"].toString() - val event = Event(remoteId, name, description, acl) + val event = Event( + remoteId, + name, + description, + acl + ) eventJson.get("style")?.asJsonObjectOrNull()?.let { style -> event.style = style.toString() } @@ -48,7 +53,7 @@ class EventDeserializer : JsonDeserializer { return event } - private fun deserializeForms(formsJson: JsonArray): Collection { + private fun deserializeForms(formsJson: JsonArray): List { val forms = mutableListOf() for (formJson in formsJson.filter { it.isJsonObject }) { forms.add(deserializeForm(formJson.asJsonObject)) diff --git a/mage/src/main/java/mil/nga/giat/mage/network/event/EventService.kt b/mage/src/main/java/mil/nga/giat/mage/network/event/EventService.kt new file mode 100644 index 000000000..f83256c78 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/event/EventService.kt @@ -0,0 +1,11 @@ +package mil.nga.giat.mage.network.event + +import mil.nga.giat.mage.database.model.event.Event +import mil.nga.giat.mage.database.model.team.Team +import retrofit2.Response +import retrofit2.http.GET + +interface EventService { + @GET("/api/events") + suspend fun getEvents(): Response> +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/event/EventsDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/network/event/EventsDeserializer.java new file mode 100644 index 000000000..5ef0571fe --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/event/EventsDeserializer.java @@ -0,0 +1,36 @@ +package mil.nga.giat.mage.network.event; + +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import mil.nga.giat.mage.database.model.event.Event; + +public class EventsDeserializer implements JsonDeserializer> { + + private final Gson eventDeserializer; + + public EventsDeserializer() { + eventDeserializer = EventDeserializer.getGsonBuilder(); + } + + @Override + public List deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + List events = new ArrayList<>(); + + for (JsonElement element : json.getAsJsonArray()) { + JsonObject jsonEvent = element.getAsJsonObject(); + Event event = eventDeserializer.fromJson(jsonEvent, Event.class); + events.add(event); + } + + return events; + } +} diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/FeedService.kt b/mage/src/main/java/mil/nga/giat/mage/network/feed/FeedService.kt similarity index 74% rename from mage/src/main/java/mil/nga/giat/mage/network/api/FeedService.kt rename to mage/src/main/java/mil/nga/giat/mage/network/feed/FeedService.kt index cd2bbdc98..ffa6e0254 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/FeedService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/feed/FeedService.kt @@ -1,7 +1,7 @@ -package mil.nga.giat.mage.network.api +package mil.nga.giat.mage.network.feed -import mil.nga.giat.mage.data.feed.Feed -import mil.nga.giat.mage.data.feed.FeedContent +import mil.nga.giat.mage.database.model.feed.Feed +import mil.nga.giat.mage.database.model.feed.FeedContent import retrofit2.Response import retrofit2.http.GET import retrofit2.http.POST diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/GeometrySerializer.java b/mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometrySerializer.java similarity index 99% rename from mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/GeometrySerializer.java rename to mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometrySerializer.java index fb96a0ae4..c0274eb94 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/GeometrySerializer.java +++ b/mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometrySerializer.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.gson.serializer; +package mil.nga.giat.mage.network.geometry; import com.google.gson.Gson; import com.google.gson.GsonBuilder; diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometryTypeAdapter.kt similarity index 98% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometryTypeAdapter.kt index fb8a23ea8..78c776543 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/geometry/GeometryTypeAdapter.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.network.gson +package mil.nga.giat.mage.network.geometry import com.google.gson.JsonArray import com.google.gson.JsonParser diff --git a/mage/src/main/java/mil/nga/giat/mage/data/gson/AnnotationExclusionStrategy.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/AnnotationExclusionStrategy.kt similarity index 90% rename from mage/src/main/java/mil/nga/giat/mage/data/gson/AnnotationExclusionStrategy.kt rename to mage/src/main/java/mil/nga/giat/mage/network/gson/AnnotationExclusionStrategy.kt index ede5648d6..20cf5b5c9 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/gson/AnnotationExclusionStrategy.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/gson/AnnotationExclusionStrategy.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.gson +package mil.nga.giat.mage.network.gson import com.google.gson.ExclusionStrategy import com.google.gson.FieldAttributes diff --git a/mage/src/main/java/mil/nga/giat/mage/data/gson/DateTimestampTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/DateTimestampTypeAdapter.kt similarity index 94% rename from mage/src/main/java/mil/nga/giat/mage/data/gson/DateTimestampTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/gson/DateTimestampTypeAdapter.kt index b4294330d..4de0d8cc7 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/gson/DateTimestampTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/gson/DateTimestampTypeAdapter.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.gson +package mil.nga.giat.mage.network.gson import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader diff --git a/mage/src/main/java/mil/nga/giat/mage/data/gson/Exclude.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/Exclude.kt similarity index 71% rename from mage/src/main/java/mil/nga/giat/mage/data/gson/Exclude.kt rename to mage/src/main/java/mil/nga/giat/mage/network/gson/Exclude.kt index 74d71fdfa..f50fdbbcf 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/gson/Exclude.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/gson/Exclude.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.gson +package mil.nga.giat.mage.network.gson @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FIELD) diff --git a/mage/src/main/java/mil/nga/giat/mage/data/gson/FeatureCollectionTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/FeatureCollectionTypeAdapter.kt similarity index 93% rename from mage/src/main/java/mil/nga/giat/mage/data/gson/FeatureCollectionTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/gson/FeatureCollectionTypeAdapter.kt index 345a12ad1..df5259c2b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/gson/FeatureCollectionTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/gson/FeatureCollectionTypeAdapter.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.gson +package mil.nga.giat.mage.network.gson import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -7,7 +7,7 @@ import com.google.gson.reflect.TypeToken import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter -import mil.nga.giat.mage.data.feed.FeedItem +import mil.nga.giat.mage.database.model.feed.FeedItem import java.lang.UnsupportedOperationException import java.lang.reflect.Type import java.util.* diff --git a/mage/src/main/java/mil/nga/giat/mage/data/gson/GeometryTypeAdapterFactory.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapterFactory.kt similarity index 99% rename from mage/src/main/java/mil/nga/giat/mage/data/gson/GeometryTypeAdapterFactory.kt rename to mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapterFactory.kt index 9a59ff77f..6b8e3894a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/data/gson/GeometryTypeAdapterFactory.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/gson/GeometryTypeAdapterFactory.kt @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.data.gson +package mil.nga.giat.mage.network.gson import com.google.gson.Gson import com.google.gson.TypeAdapter diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/user/UsersTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/gson/user/UsersTypeAdapter.kt deleted file mode 100644 index 2f2bf415b..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/user/UsersTypeAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package mil.nga.giat.mage.network.gson.user - -import android.content.Context -import com.google.gson.TypeAdapter -import com.google.gson.stream.JsonReader -import com.google.gson.stream.JsonToken -import com.google.gson.stream.JsonWriter -import mil.nga.giat.mage.sdk.datastore.user.User - -class UsersTypeAdapter(val context: Context): TypeAdapter>() { - private val userTypeAdapter = UserTypeAdapter(context) - - override fun write(out: JsonWriter, value: List) { - throw UnsupportedOperationException() - } - - override fun read(reader: JsonReader): List { - val users = mutableListOf() - - if (reader.peek() != JsonToken.BEGIN_ARRAY) { - reader.skipValue() - return users - } - - reader.beginArray() - - while (reader.hasNext()) { - users.add(userTypeAdapter.read(reader)) - } - - reader.endArray() - - return users - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayerDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayerDeserializer.java similarity index 85% rename from mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayerDeserializer.java rename to mage/src/main/java/mil/nga/giat/mage/network/layer/LayerDeserializer.java index f916a2dbd..fc3801222 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayerDeserializer.java +++ b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayerDeserializer.java @@ -1,4 +1,4 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; +package mil.nga.giat.mage.network.layer; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -10,19 +10,10 @@ import java.lang.reflect.Type; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; +import mil.nga.giat.mage.database.model.layer.Layer; -/** - * JSON to {@link Layer} - */ public class LayerDeserializer implements JsonDeserializer { - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Layer}. - */ public static Gson getGsonBuilder() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(Layer.class, new LayerDeserializer()); diff --git a/mage/src/main/java/mil/nga/giat/mage/network/layer/LayerService.kt b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayerService.kt new file mode 100644 index 000000000..df2460fdf --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayerService.kt @@ -0,0 +1,25 @@ +package mil.nga.giat.mage.network.layer + +import mil.nga.giat.mage.database.model.layer.Layer +import mil.nga.giat.mage.database.model.feature.StaticFeature +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Url + +interface LayerService { + + @GET("/api/events/{eventId}/layers") + suspend fun getLayers(@Path("eventId") eventId: String?, @Query("type") type: String?): Response> + + @GET("/api/events/{eventId}/layers/{layerId}/features") + suspend fun getFeatures( + @Path("eventId") eventId: String, + @Path("layerId") layerId: String + ): Response> + + @GET + suspend fun getFeatureIcon(@Url url: String): Response +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayersDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayersDeserializer.kt similarity index 66% rename from mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayersDeserializer.kt rename to mage/src/main/java/mil/nga/giat/mage/network/layer/LayersDeserializer.kt index be83563fe..07c069236 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/LayersDeserializer.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/layer/LayersDeserializer.kt @@ -1,14 +1,14 @@ -package mil.nga.giat.mage.sdk.gson.deserializer +package mil.nga.giat.mage.network.layer import com.google.gson.* -import mil.nga.giat.mage.sdk.datastore.layer.Layer +import mil.nga.giat.mage.database.model.layer.Layer import java.lang.reflect.Type -class LayersDeserializer : JsonDeserializer> { +class LayersDeserializer : JsonDeserializer> { private val layerDeserializer: Gson = LayerDeserializer.getGsonBuilder() @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Collection { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): List { val layers = mutableListOf() for (element in json.asJsonArray) { layers.add(layerDeserializer.fromJson(element.asJsonObject, Layer::class.java)) diff --git a/mage/src/main/java/mil/nga/giat/mage/network/location/LocationService.kt b/mage/src/main/java/mil/nga/giat/mage/network/location/LocationService.kt new file mode 100644 index 000000000..490c39d22 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/location/LocationService.kt @@ -0,0 +1,21 @@ +package mil.nga.giat.mage.network.location + +import mil.nga.giat.mage.database.model.location.Location +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface LocationService { + + @GET("/api/events/{eventId}/locations/users") + suspend fun getLocations(@Path("eventId") eventId: String?): Response> + + @POST("/api/events/{eventId}/locations") + @JvmSuppressWildcards + suspend fun pushLocations( + @Path("eventId") eventId: String, + @Body locations: @JvmSuppressWildcards List + ): Response> +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/LocationsTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/location/LocationsTypeAdapter.kt similarity index 71% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/LocationsTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/location/LocationsTypeAdapter.kt index 8d89e7073..2c64b403a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/LocationsTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/location/LocationsTypeAdapter.kt @@ -1,12 +1,15 @@ -package mil.nga.giat.mage.network.gson - import android.util.Log import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.location.LocationProperty +import mil.nga.giat.mage.network.geometry.GeometrySerializer +import mil.nga.giat.mage.network.geometry.GeometryTypeAdapter +import mil.nga.giat.mage.network.gson.nextBooleanOrNull +import mil.nga.giat.mage.network.gson.nextNumberOrNull +import mil.nga.giat.mage.network.gson.nextStringOrNull import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory import java.io.IOException import java.io.Serializable @@ -15,10 +18,6 @@ import java.text.ParseException class LocationsTypeAdapter: TypeAdapter>() { private val geometryDeserializer = GeometryTypeAdapter() - override fun write(out: JsonWriter, value: List) { - throw UnsupportedOperationException() - } - override fun read(reader: JsonReader): List { val locations = mutableListOf() @@ -30,49 +29,6 @@ class LocationsTypeAdapter: TypeAdapter>() { reader.beginArray() while (reader.hasNext()) { - locations.addAll(readUserLocations(reader)) - } - - reader.endArray() - - return locations - } - - @Throws(IOException::class) - private fun readUserLocations(reader: JsonReader): List { - val locations = mutableListOf() - - if (reader.peek() != JsonToken.BEGIN_OBJECT) { - reader.skipValue() - return locations - } - - reader.beginObject() - - while(reader.hasNext()) { - when(reader.nextName()) { - "locations" -> locations.addAll(readLocations(reader)) - else -> reader.skipValue() - } - } - - reader.endObject() - - return locations - } - - @Throws(IOException::class) - private fun readLocations(reader: JsonReader): List { - val locations = mutableListOf() - - if (reader.peek() != JsonToken.BEGIN_ARRAY) { - reader.skipValue() - return locations - } - - reader.beginArray() - - while(reader.hasNext()) { locations.add(readLocation(reader)) } @@ -146,7 +102,11 @@ class LocationsTypeAdapter: TypeAdapter>() { } if (value != null) { - val property = LocationProperty(key, value) + val property = + LocationProperty( + key, + value + ) property.location = location properties.add(property) } @@ -158,6 +118,32 @@ class LocationsTypeAdapter: TypeAdapter>() { return properties } + override fun write(out: JsonWriter, value: List) { + out.beginArray() + + value.forEach { location -> + out.beginObject() + out.name("eventId").value(location.event.remoteId.toInt()) + out.name("geometry").jsonValue(GeometrySerializer.getGsonBuilder().toJson(location.geometry)) + + out.name("properties").beginObject() + out.name("timestamp").value(ISO8601DateFormatFactory.ISO8601().format(location.timestamp)) + location.properties.filter { it.value != null }.forEach { property -> + out.name(property.key) + when (val propertyValue = property.value) { + is Double -> out.value(propertyValue) + is Float -> out.value(propertyValue) + is Boolean -> out.value(propertyValue) + else -> out.value(property.value.toString()) + } + } + out.endObject() + out.endObject() + } + + out.endArray() + } + companion object { private val LOG_NAME = LocationsTypeAdapter::class.java.name } diff --git a/mage/src/main/java/mil/nga/giat/mage/network/location/UserLocationsTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/location/UserLocationsTypeAdapter.kt new file mode 100644 index 000000000..66588a136 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/location/UserLocationsTypeAdapter.kt @@ -0,0 +1,75 @@ +package mil.nga.giat.mage.network.location + +import LocationsTypeAdapter +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.network.gson.nextStringOrNull +import java.io.IOException +import java.lang.UnsupportedOperationException + +data class UserLocations( + val userId: String, + val locations: List +) + +class UserLocationsTypeAdapter: TypeAdapter>() { + private val locationsTypeAdapter = LocationsTypeAdapter() + + override fun read(reader: JsonReader): List { + val locations = mutableListOf() + + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + reader.skipValue() + return locations + } + + reader.beginArray() + + while (reader.hasNext()) { + readUserLocations(reader)?.let { + locations.add(it) + } + } + + reader.endArray() + + return locations + } + + @Throws(IOException::class) + private fun readUserLocations(reader: JsonReader): UserLocations? { + var userId: String? = null + val locations = mutableListOf() + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + reader.skipValue() + return null + } + + reader.beginObject() + + while(reader.hasNext()) { + when(reader.nextName()) { + "userId" -> userId = reader.nextStringOrNull() + "locations" -> locations.addAll(locationsTypeAdapter.read(reader)) + else -> reader.skipValue() + } + } + + reader.endObject() + + return userId?.let { + UserLocations( + userId = it, + locations = locations + ) + } + } + + override fun write(out: JsonWriter?, value: List?) { + throw UnsupportedOperationException() + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationDeserializer.kt similarity index 79% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationDeserializer.kt rename to mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationDeserializer.kt index 413a9f7a0..70dea34ab 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationDeserializer.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationDeserializer.kt @@ -1,12 +1,24 @@ -package mil.nga.giat.mage.network.gson.observation +package mil.nga.giat.mage.network.observation import android.util.Log import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.database.model.observation.ObservationForm +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty +import mil.nga.giat.mage.database.model.observation.State +import mil.nga.giat.mage.network.attachment.AttachmentTypeAdapter import mil.nga.giat.mage.network.gson.* -import mil.nga.giat.mage.sdk.datastore.observation.* -import mil.nga.giat.mage.sdk.utils.GeometryUtility +import mil.nga.giat.mage.network.geometry.GeometryTypeAdapter +import mil.nga.giat.mage.network.gson.nextBooleanOrNull +import mil.nga.giat.mage.network.gson.nextDoubleOrNull +import mil.nga.giat.mage.network.gson.nextNumberOrNull +import mil.nga.giat.mage.network.gson.nextStringOrNull import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import mil.nga.giat.mage.sdk.utils.toBytes import java.io.IOException import java.text.ParseException import java.util.* @@ -16,7 +28,8 @@ class ObservationDeserializer { private val attachmentDeserializer = AttachmentTypeAdapter() fun read(reader: JsonReader): Observation { - val observation = Observation() + val observation = + Observation() if (reader.peek() != JsonToken.BEGIN_OBJECT) { reader.skipValue() @@ -89,7 +102,7 @@ class ObservationDeserializer { } @Throws(IOException::class) - private fun readProperties(reader: JsonReader, observation: Observation): Collection { + private fun readProperties(reader: JsonReader, observation: Observation): List { val properties = mutableListOf() if (reader.peek() != JsonToken.BEGIN_OBJECT) { @@ -184,8 +197,12 @@ class ObservationDeserializer { when(reader.peek()) { JsonToken.BEGIN_OBJECT -> { geometryDeserializer.read(reader)?.let { geometry -> - val geometryBytes = GeometryUtility.toGeometryBytes(geometry) - property = ObservationProperty(key, geometryBytes) + val geometryBytes = geometry.toBytes() + property = + ObservationProperty( + key, + geometryBytes + ) } } JsonToken.BEGIN_ARRAY -> { @@ -204,26 +221,46 @@ class ObservationDeserializer { } if (stringArrayList.isNotEmpty()) { - property = ObservationProperty(key, stringArrayList) + property = + ObservationProperty( + key, + stringArrayList + ) } else if (attachments.isNotEmpty()) { - property = ObservationProperty(key, attachments) + property = + ObservationProperty( + key, + attachments + ) } reader.endArray() } JsonToken.NUMBER -> { reader.nextNumberOrNull()?.let { - property = ObservationProperty(key, it) + property = + ObservationProperty( + key, + it + ) } } JsonToken.BOOLEAN -> { reader.nextBooleanOrNull()?.let { - property = ObservationProperty(key, it) + property = + ObservationProperty( + key, + it + ) } } JsonToken.STRING -> { reader.nextStringOrNull()?.let { - property = ObservationProperty(key, it) + property = + ObservationProperty( + key, + it + ) } } else -> reader.skipValue() @@ -257,7 +294,8 @@ class ObservationDeserializer { @Throws(IOException::class) private fun readImportant(reader: JsonReader): ObservationImportant { - val important = ObservationImportant() + val important = + ObservationImportant() important.isImportant = true important.isDirty = false @@ -289,7 +327,7 @@ class ObservationDeserializer { } @Throws(IOException::class) - private fun readFavoriteUsers(reader: JsonReader): Collection { + private fun readFavoriteUsers(reader: JsonReader): List { val favorites = mutableListOf() if (reader.peek() != JsonToken.BEGIN_ARRAY) { @@ -302,7 +340,11 @@ class ObservationDeserializer { while(reader.hasNext()) { if (reader.peek() == JsonToken.STRING) { val userId = reader.nextString() - val favorite = ObservationFavorite(userId, true) + val favorite = + ObservationFavorite( + userId, + true + ) favorite.isDirty = false favorites.add(favorite) } else reader.skipValue() diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationSerializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationSerializer.kt similarity index 93% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationSerializer.kt rename to mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationSerializer.kt index b2ea62fc3..d332d8899 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationSerializer.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationSerializer.kt @@ -1,11 +1,11 @@ -package mil.nga.giat.mage.network.gson.observation +package mil.nga.giat.mage.network.observation import android.util.Log import com.google.gson.* -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.gson.serializer.GeometrySerializer -import mil.nga.giat.mage.sdk.utils.GeometryUtility +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.network.geometry.GeometrySerializer import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import mil.nga.giat.mage.sdk.utils.toGeometry import java.lang.reflect.Type import java.util.* @@ -90,7 +90,7 @@ class ObservationSerializer : JsonSerializer { "geometry" -> { try { val bytes = value as ByteArray - val geometry = GeometryUtility.toGeometry(bytes) + val geometry = bytes.toGeometry() JsonParser.parseString(GeometrySerializer.getGsonBuilder().toJson(geometry)) } catch (e: Exception) { Log.w(LOG_NAME, "Error converting byte array to geometry", e) diff --git a/mage/src/main/java/mil/nga/giat/mage/network/api/ObservationService.kt b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationService.kt similarity index 68% rename from mage/src/main/java/mil/nga/giat/mage/network/api/ObservationService.kt rename to mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationService.kt index b3709d01f..4177df191 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/api/ObservationService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationService.kt @@ -1,7 +1,9 @@ -package mil.nga.giat.mage.network.api +package mil.nga.giat.mage.network.observation import com.google.gson.JsonObject -import mil.nga.giat.mage.sdk.datastore.observation.Observation +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Observation +import okhttp3.RequestBody import okhttp3.ResponseBody import retrofit2.Response import retrofit2.http.* @@ -22,7 +24,8 @@ interface ObservationService { suspend fun updateObservation( @Path("eventId") eventId: String, @Path("observationId") observationId: String, - @Body observation: Observation): Response + @Body observation: Observation + ): Response @POST("/api/events/{eventId}/observations/{observationId}/states") suspend fun archiveObservation( @@ -55,4 +58,21 @@ interface ObservationService { @Path("eventId") eventId: String, @Path("observationId") observationId: String ): Response + + @Streaming + @GET("/api/events/{eventId}/observations/{observationId}/attachments/{attachmentId}") + suspend fun getAttachment( + @Path("eventId") eventId: String, + @Path("observationId") observationId: String, + @Path("attachmentId") attachmentId: String + ): Response + + @Multipart + @PUT("/api/events/{eventId}/observations/{observationId}/attachments/{attachmentId}") + suspend fun createAttachment( + @Path("eventId") eventId: String, + @Path("observationId") observationId: String, + @Path("attachmentId") attachmentId: String, + @PartMap parts: Map + ): Response } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationTypeAdapter.kt similarity index 82% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationTypeAdapter.kt index f4094a8d3..ceb3c7cb1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationTypeAdapter.kt @@ -1,11 +1,10 @@ -package mil.nga.giat.mage.network.gson.observation +package mil.nga.giat.mage.network.observation -import android.content.Context import com.google.gson.GsonBuilder import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonWriter -import mil.nga.giat.mage.sdk.datastore.observation.Observation +import mil.nga.giat.mage.database.model.observation.Observation class ObservationTypeAdapter: TypeAdapter() { private val observationSerializer = GsonBuilder() diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationsTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationsTypeAdapter.kt similarity index 88% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationsTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationsTypeAdapter.kt index e87c94610..39a5efe8a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/observation/ObservationsTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/observation/ObservationsTypeAdapter.kt @@ -1,10 +1,10 @@ -package mil.nga.giat.mage.network.gson.observation +package mil.nga.giat.mage.network.observation import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter -import mil.nga.giat.mage.sdk.datastore.observation.Observation +import mil.nga.giat.mage.database.model.observation.Observation class ObservationsTypeAdapter: TypeAdapter>() { private val observationDeserializer = ObservationDeserializer() diff --git a/mage/src/main/java/mil/nga/giat/mage/network/role/RoleService.kt b/mage/src/main/java/mil/nga/giat/mage/network/role/RoleService.kt new file mode 100644 index 000000000..1c0890140 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/role/RoleService.kt @@ -0,0 +1,10 @@ +package mil.nga.giat.mage.network.role + +import mil.nga.giat.mage.database.model.permission.Role +import retrofit2.Response +import retrofit2.http.GET + +interface RoleService { + @GET("/api/roles") + suspend fun getRoles(): Response> +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/role/RolesDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/role/RolesDeserializer.kt new file mode 100644 index 000000000..a5d061ba5 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/role/RolesDeserializer.kt @@ -0,0 +1,54 @@ +package mil.nga.giat.mage.network.role + +import android.util.Log +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import mil.nga.giat.mage.database.model.permission.Permission +import mil.nga.giat.mage.database.model.permission.Permissions +import mil.nga.giat.mage.database.model.permission.Role +import java.lang.reflect.Type + +class RolesDeserializer : JsonDeserializer> { + + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): List { + val roles = mutableListOf() + val jsonRoles = json.asJsonArray + for (element in jsonRoles) { + val role = deserializeRole(element) + roles.add(role) + } + return roles + } + + private fun deserializeRole(json: JsonElement): Role { + val jsonRole = json.asJsonObject + val remoteId = jsonRole["id"].asString + val name = jsonRole["name"].asString + val description = jsonRole["description"].asString + val permissions = mutableListOf() + + jsonRole["permissions"].asJsonArray.forEach { element -> + element?.asString?.let { jsonPermission -> + try { + val permission = Permission.valueOf(jsonPermission.uppercase()) + permissions.add(permission) + } catch (iae: IllegalArgumentException) { + Log.e(LOG_NAME, "Could not find matching permission, $jsonPermission, for user.") + } + } + } + + return Role(remoteId, name, description, Permissions(permissions)) + } + + companion object { + private val LOG_NAME = RolesDeserializer::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/team/TeamDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamDeserializer.kt new file mode 100644 index 000000000..a80939787 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamDeserializer.kt @@ -0,0 +1,37 @@ +package mil.nga.giat.mage.network.team + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import mil.nga.giat.mage.database.model.team.Team +import java.lang.reflect.Type + +class TeamDeserializer : JsonDeserializer { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): Team { + val jsonTeam = json.asJsonObject + val remoteId = jsonTeam["id"].asString + val name = jsonTeam["name"].asString + var description = "" + if (jsonTeam.has("description")) { + description = jsonTeam["description"].toString() + } + return Team(remoteId, name, description) + } + + companion object { + val gsonBuilder: Gson + get() { + val gsonBuilder = GsonBuilder() + gsonBuilder.registerTypeAdapter(Team::class.java, TeamDeserializer()) + return gsonBuilder.create() + } + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/team/TeamService.kt b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamService.kt new file mode 100644 index 000000000..3607cf76d --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamService.kt @@ -0,0 +1,15 @@ +package mil.nga.giat.mage.network.team + +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.network.user.UserWithRoleId +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path + +interface TeamService { + + @GET("/api/events/{eventId}/teams?populate=users") + suspend fun getTeams(@Path("eventId") eventId: String?): Response>> + +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/team/TeamsDeserializer.kt b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamsDeserializer.kt new file mode 100644 index 000000000..b0af9ecda --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/team/TeamsDeserializer.kt @@ -0,0 +1,54 @@ +package mil.nga.giat.mage.network.team + +import android.util.Log +import com.google.gson.JsonArray +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException +import com.google.gson.stream.JsonReader +import mil.nga.giat.mage.data.repository.event.EventRepository +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.network.user.UserWithRoleId +import mil.nga.giat.mage.network.user.UserWithRoleIdTypeAdapter +import java.io.StringReader +import java.lang.reflect.Type + +class TeamsDeserializer : JsonDeserializer>> { + private val userTypeAdapter = UserWithRoleIdTypeAdapter() + private val teamDeserializer = TeamDeserializer.gsonBuilder + + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): Map> { + val teams = mutableMapOf>() + for (element in json.asJsonArray) { + val jsonTeam = element.asJsonObject + val team = teamDeserializer.fromJson(jsonTeam, Team::class.java) + val users = deserializeUsers(jsonTeam.getAsJsonArray("users")) + teams[team] = users + } + return teams + } + + private fun deserializeUsers(jsonUsers: JsonArray): List { + val users: MutableList = ArrayList() + for (userElement in jsonUsers) { + val jsonUser = userElement.asJsonObject + try { + val reader = JsonReader(StringReader(jsonUser.toString())) + users.add(userTypeAdapter.read(reader)) + } catch (e: Exception) { + Log.e(LOG_NAME, "Error deserializing user", e) + } + } + return users + } + + companion object { + private val LOG_NAME = TeamsDeserializer::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/user/UserService.kt b/mage/src/main/java/mil/nga/giat/mage/network/user/UserService.kt new file mode 100644 index 000000000..04a47c540 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/user/UserService.kt @@ -0,0 +1,52 @@ +package mil.nga.giat.mage.network.user + +import com.google.gson.JsonObject +import mil.nga.giat.mage.database.model.user.User +import okhttp3.RequestBody +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.PartMap +import retrofit2.http.Path + +interface UserService { + + @POST("/auth/{strategy}/signin") + suspend fun signin( + @Path("strategy") strategy: String, + @Body parameters: JsonObject): Response + + @POST("/api/logout") + fun signout(): Call + + @POST("/api/users/signups") + suspend fun signup(@Body body: JsonObject): Response + + @POST("/api/users/signups/verifications") + suspend fun signupVerify( + @Header("Authorization") authorization: String, + @Body body: JsonObject + ): Response + + @GET("/api/users/{userId}") + suspend fun getUser(@Path("userId") userId: String): Response + + @PUT("/api/users/myself/password") + suspend fun changePassword(@Body body: JsonObject): Response + + @GET("/api/users/{userId}/icon") + suspend fun getIcon(@Path("userId") userId: String?): Response + + @POST("/api/users/{userId}/events/{eventId}/recent") + suspend fun addRecentEvent(@Path("userId") userId: String, @Path("eventId") eventId: String): Response + + @Multipart + @PUT("/api/users/myself") + suspend fun createAvatar(@PartMap parts: Map): Response +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleIdTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleIdTypeAdapter.kt new file mode 100644 index 000000000..7ec11f70b --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleIdTypeAdapter.kt @@ -0,0 +1,130 @@ +package mil.nga.giat.mage.network.user + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import mil.nga.giat.mage.database.model.user.Phone +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.network.gson.nextStringOrNull +import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory +import org.apache.commons.lang3.StringUtils +import java.io.IOException +import java.text.ParseException +import java.util.* + +data class UserWithRoleId( + var user: User, + var roleId: String, +) + +class UserWithRoleIdTypeAdapter: TypeAdapter() { + + override fun write(out: JsonWriter, value: UserWithRoleId) { + throw UnsupportedOperationException() + } + + override fun read(reader: JsonReader): UserWithRoleId { + val user = User() + var roleId = "" + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + reader.skipValue() + return UserWithRoleId(user, roleId) + } + + reader.beginObject() + while(reader.hasNext()) { + when(reader.nextName()) { + "id" -> user.remoteId = reader.nextString() + "username" -> user.username = reader.nextString() + "displayName" -> user.displayName = reader.nextString() + "email" -> user.email = reader.nextStringOrNull() + "avatarUrl" -> user.avatarUrl = reader.nextStringOrNull() + "iconUrl" -> user.iconUrl = reader.nextStringOrNull() + "phones" -> user.primaryPhone = parsePrimaryPhone(reader) + "roleId" -> roleId = reader.nextString() + "recentEventIds" -> user.setRecentEventIds(readRecentEventIds(reader)) + "lastUpdated" -> { + try { + user.lastModified = ISO8601DateFormatFactory.ISO8601().parse(reader.nextString()) + } catch (_: ParseException) { } + } + else -> reader.skipValue() + } + } + + user.role + + reader.endObject() + + return UserWithRoleId(user, roleId) + } + + @Throws(IOException::class) + private fun parsePrimaryPhone(reader: JsonReader): String? { + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + reader.skipValue() + return null + } + + reader.beginArray() + + var primaryPhone: String? = null + while(reader.hasNext()) { + val phone = readPhone(reader) + if (primaryPhone == null) { + primaryPhone = phone.number + } + } + + reader.endArray() + + return primaryPhone + } + + @Throws(IOException::class) + private fun readPhone(reader: JsonReader): Phone { + val phone = Phone() + + if (reader.peek() != JsonToken.BEGIN_OBJECT) { + reader.skipValue() + return phone + } + + reader.beginObject() + + while(reader.hasNext()) { + val name = reader.nextName() + if ("number" == name) { + phone.number = reader.nextString() + } else { + reader.skipValue() + } + } + + reader.endObject() + + return phone + } + + @Throws(IOException::class) + private fun readRecentEventIds(reader: JsonReader): String { + if (reader.peek() != JsonToken.BEGIN_ARRAY) { + reader.skipValue() + return "" + } + + val recentEventIds = mutableListOf() + + reader.beginArray() + + while (reader.hasNext()) { + recentEventIds.add(reader.nextString()) + } + + reader.endArray() + + return StringUtils.join(recentEventIds, ",") + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/network/gson/user/UserTypeAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleTypeAdapter.kt similarity index 76% rename from mage/src/main/java/mil/nga/giat/mage/network/gson/user/UserTypeAdapter.kt rename to mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleTypeAdapter.kt index 852bf2364..71b16bfe3 100644 --- a/mage/src/main/java/mil/nga/giat/mage/network/gson/user/UserTypeAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/network/user/UserWithRoleTypeAdapter.kt @@ -1,33 +1,40 @@ -package mil.nga.giat.mage.network.gson.user +package mil.nga.giat.mage.network.user -import android.content.Context import android.util.Log import com.google.gson.TypeAdapter import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonToken import com.google.gson.stream.JsonWriter +import mil.nga.giat.mage.database.model.permission.Permission +import mil.nga.giat.mage.database.model.permission.Permissions +import mil.nga.giat.mage.database.model.permission.Role +import mil.nga.giat.mage.database.model.user.Phone +import mil.nga.giat.mage.database.model.user.User import mil.nga.giat.mage.network.gson.nextStringOrNull -import mil.nga.giat.mage.sdk.datastore.user.* -import mil.nga.giat.mage.sdk.exceptions.RoleException import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory import org.apache.commons.lang3.StringUtils import java.io.IOException import java.text.ParseException import java.util.* -class UserTypeAdapter(val context: Context): TypeAdapter() { - private val roleHelper = RoleHelper.getInstance(context) +data class UserWithRole( + var user: User, + var role: Role, +) - override fun write(out: JsonWriter, value: User) { +class UserWithRoleTypeAdapter: TypeAdapter() { + + override fun write(out: JsonWriter, value: UserWithRole) { throw UnsupportedOperationException() } - override fun read(reader: JsonReader): User { + override fun read(reader: JsonReader): UserWithRole { val user = User() + var role = Role() if (reader.peek() != JsonToken.BEGIN_OBJECT) { reader.skipValue() - return user + return UserWithRole(user, role) } reader.beginObject() @@ -40,21 +47,22 @@ class UserTypeAdapter(val context: Context): TypeAdapter() { "avatarUrl" -> user.avatarUrl = reader.nextStringOrNull() "iconUrl" -> user.iconUrl = reader.nextStringOrNull() "phones" -> user.primaryPhone = parsePrimaryPhone(reader) - "role" -> user.role = readRole(reader) - "roleId" -> user.role = readRoleId(reader) + "role" -> role = readRole(reader) "recentEventIds" -> user.setRecentEventIds(readRecentEventIds(reader)) "lastUpdated" -> { try { user.lastModified = ISO8601DateFormatFactory.ISO8601().parse(reader.nextString()) - } catch (e: ParseException) { } + } catch (_: ParseException) { } } else -> reader.skipValue() } } + user.role + reader.endObject() - return user + return UserWithRole(user, role) } @Throws(IOException::class) @@ -105,27 +113,14 @@ class UserTypeAdapter(val context: Context): TypeAdapter() { } @Throws(IOException::class) - private fun readRoleId(reader: JsonReader): Role? { - val roleId = reader.nextString() - var role: Role? = null - try { - role = roleHelper.read(roleId) - } catch (e: RoleException) { - Log.e(LOG_NAME, "Could not find matching role for user.") - } - - return role - } + private fun readRole(reader: JsonReader): Role { + val role = Role() - @Throws(IOException::class) - private fun readRole(reader: JsonReader): Role? { if (reader.peek() != JsonToken.BEGIN_OBJECT) { reader.skipValue() - return null + return role } - val role = Role() - reader.beginObject() while (reader.hasNext()) { @@ -138,20 +133,18 @@ class UserTypeAdapter(val context: Context): TypeAdapter() { } } - roleHelper.createOrUpdate(role) - reader.endObject() return role } @Throws(IOException::class) - private fun readPermissions(reader: JsonReader): Permissions? { - val permissions: MutableCollection = ArrayList() + private fun readPermissions(reader: JsonReader): Permissions { + val permissions = mutableListOf() if (reader.peek() != JsonToken.BEGIN_ARRAY) { reader.skipValue() - return null + return Permissions() } reader.beginArray() @@ -191,6 +184,6 @@ class UserTypeAdapter(val context: Context): TypeAdapter() { } companion object { - private val LOG_NAME = UserTypeAdapter::class.java.name + private val LOG_NAME = UserWithRoleTypeAdapter::class.java.name } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedFragment.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedFragment.kt index e4484e4a2..51716666b 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedFragment.kt @@ -24,6 +24,7 @@ import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.LandingViewModel import mil.nga.giat.mage.R import mil.nga.giat.mage.coordinate.CoordinateFormatter +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource import mil.nga.giat.mage.filter.ObservationFilterActivity import mil.nga.giat.mage.location.LocationAccess import mil.nga.giat.mage.location.LocationPolicy @@ -34,9 +35,10 @@ import mil.nga.giat.mage.observation.ObservationLocation import mil.nga.giat.mage.observation.attachment.AttachmentViewActivity import mil.nga.giat.mage.observation.edit.ObservationEditActivity import mil.nga.giat.mage.observation.view.ObservationViewActivity -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.utils.googleMapsUri import javax.inject.Inject @@ -53,6 +55,11 @@ class ObservationFeedFragment : Fragment() { private lateinit var attachmentGallery: AttachmentGallery private var listState: Parcelable? = null + @Inject lateinit var userLocalDataSource: UserLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + @Inject lateinit var locationLocalDataSource: LocationLocalDataSource + @Inject lateinit var observationLocalDataSource: ObservationLocalDataSource + @Inject lateinit var locationAccess: LocationAccess @Inject lateinit var locationPolicy: LocationPolicy private lateinit var locationProvider: LiveData @@ -93,6 +100,9 @@ class ObservationFeedFragment : Fragment() { viewModel.observationFeedState.observe(viewLifecycleOwner) { feedState -> recyclerView.adapter = ObservationListAdapter( requireContext(), + userLocalDataSource, + eventLocalDataSource, + observationLocalDataSource, feedState, attachmentGallery, object : ObservationActionListener { @@ -171,7 +181,7 @@ class ObservationFeedFragment : Fragment() { private fun onNewObservation() { val location = getLocation() - if (!UserHelper.getInstance(context).isCurrentUserPartOfCurrentEvent) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { AlertDialog.Builder(requireActivity(), R.style.AppCompatAlertDialogStyle) .setTitle(requireActivity().resources.getString(R.string.no_event_title)) .setMessage(requireActivity().resources.getString(R.string.observation_no_event_message)) @@ -204,11 +214,12 @@ class ObservationFeedFragment : Fragment() { } private fun getLocation(): ObservationLocation? { + val user = userLocalDataSource.readCurrentUser() ?: return null var observationLocation: ObservationLocation? = null // if there is not a location from the location service, then try to pull one from the database. if (locationProvider.value == null) { - val locations = LocationHelper.getInstance(context).getCurrentUserLocations(1, true) + val locations = locationLocalDataSource.getCurrentUserLocations(user, 1, true) locations.firstOrNull()?.let { location -> val provider = location.propertiesMap["provider"]?.value?.toString() ?: ObservationLocation.MANUAL_PROVIDER diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedViewModel.kt index 48637330f..e922a960f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationFeedViewModel.kt @@ -13,19 +13,14 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import mil.nga.giat.mage.LandingViewModel import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.observation.ObservationRepository -import mil.nga.giat.mage.map.FeedItemId -import mil.nga.giat.mage.map.annotation.MapAnnotation -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper -import mil.nga.giat.mage.sdk.datastore.observation.State -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.repository.observation.ObservationRepository +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.State +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.event.IObservationEventListener import java.sql.SQLException import java.util.* @@ -35,17 +30,18 @@ import javax.inject.Inject class ObservationFeedViewModel @Inject constructor( val application: Application, val preferences: SharedPreferences, - private val observationRepository: ObservationRepository + private val observationDao: Dao, + private val observationImportantDao: Dao, + private val observationFavoriteDao: Dao, + private val observationLocalDataSource: ObservationLocalDataSource, + private val observationRepository: ObservationRepository, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource ): ViewModel() { enum class RefreshState { LOADING, COMPLETE } data class ObservationFeedState(val cursor: Cursor, val query: PreparedQuery, val filterText: String) - private val observationDao: Dao = DaoStore.getInstance(application).observationDao - private val eventHelper = EventHelper.getInstance(application) - private val userHelper = UserHelper.getInstance(application) - private val observationHelper = ObservationHelper.getInstance(application) - private var requeryTime: Long = 0 private var refreshJob: Job? = null @@ -79,14 +75,14 @@ class ObservationFeedViewModel @Inject constructor( init { filter.value = getTimeFilterId() - observationHelper.addListener(observationListener) + observationLocalDataSource.addListener(observationListener) preferences.registerOnSharedPreferenceChangeListener(sharedPreferencesChangeListener) } override fun onCleared() { super.onCleared() - observationHelper.removeListener(observationListener) + observationLocalDataSource.removeListener(observationListener) preferences.unregisterOnSharedPreferenceChangeListener(sharedPreferencesChangeListener) } @@ -104,7 +100,7 @@ class ObservationFeedViewModel @Inject constructor( @Throws(SQLException::class) private fun query(filterId: Int): ObservationFeedState { - val currentUser = userHelper.readCurrentUser() + val currentUser = userLocalDataSource.readCurrentUser() val qb: QueryBuilder = observationDao.queryBuilder() val calendar = Calendar.getInstance() @@ -151,13 +147,12 @@ class ObservationFeedViewModel @Inject constructor( .and() .ge("timestamp", calendar.time) .and() - .eq("event_id", eventHelper.currentEvent.id) + .eq("event_id", eventLocalDataSource.currentEvent?.id) val actionFilters: MutableList = ArrayList() val favorites: Boolean = preferences.getBoolean(application.resources.getString(R.string.activeFavoritesFilterKey), false) if (favorites && currentUser != null) { - val observationFavoriteDao = DaoStore.getInstance(application).observationFavoriteDao val favoriteQb = observationFavoriteDao.queryBuilder() favoriteQb.where() .eq("user_id", currentUser.remoteId) @@ -169,7 +164,6 @@ class ObservationFeedViewModel @Inject constructor( val important: Boolean = preferences.getBoolean(application.resources.getString(R.string.activeImportantFilterKey), false) if (important) { - val observationImportantDao = DaoStore.getInstance(application).observationImportantDao val importantQb = observationImportantDao.queryBuilder() importantQb.where().eq("is_important", true) qb.join(importantQb) diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationListAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationListAdapter.kt index 7db8fdfd5..b7cd73026 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationListAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/ObservationListAdapter.kt @@ -20,12 +20,16 @@ import com.j256.ormlite.android.AndroidDatabaseResults import com.j256.ormlite.stmt.PreparedQuery import mil.nga.giat.mage.R import mil.nga.giat.mage.coordinate.CoordinateFormatter +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.ObservationImportant +import mil.nga.giat.mage.database.model.observation.ObservationProperty import mil.nga.giat.mage.map.annotation.MapAnnotation import mil.nga.giat.mage.observation.attachment.AttachmentGallery -import mil.nga.giat.mage.sdk.datastore.observation.* -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.exceptions.ObservationException import mil.nga.giat.mage.sdk.exceptions.UserException import mil.nga.giat.mage.utils.DateFormatFactory @@ -35,6 +39,9 @@ import java.util.* class ObservationListAdapter( private val context: Context, + private val userLocalDataSource: UserLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val observationLocalDataSource: ObservationLocalDataSource, observationFeedState: ObservationFeedViewModel.ObservationFeedState, private val attachmentGallery: AttachmentGallery, private val observationActionListener: ObservationActionListener? @@ -50,6 +57,7 @@ class ObservationListAdapter( private val query: PreparedQuery = observationFeedState.query private val filterText: String = observationFeedState.filterText private var currentUser: User? = null + private val event = eventLocalDataSource.currentEvent private inner class ObservationViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val card: View = view.findViewById(R.id.card) @@ -141,18 +149,32 @@ class ObservationListAdapter( DrawableCompat.setTintMode(markerPlaceholder, PorterDuff.Mode.SRC_IN) vh.markerView.setImageDrawable(markerPlaceholder) + val observationForm = observation.forms.firstOrNull() + val formDefinition = observationForm?.formId?.let { formId -> + eventLocalDataSource.getForm(formId) + } + + val icon = MapAnnotation.fromObservation( + event = event, + observation = observation, + formDefinition = formDefinition, + observationForm = observationForm, + geometryType = observation.geometry.geometryType, + context = context + ) + Glide.with(context) .asBitmap() - .load(MapAnnotation.fromObservation(observation, context)) + .load(icon) .error(R.drawable.default_marker) .into(vh.markerView) vh.primaryView.text = "" - vh.primaryPropertyTask = PropertyTask(context, PropertyTask.Type.PRIMARY, vh.primaryView) + vh.primaryPropertyTask = PropertyTask(eventLocalDataSource, PropertyTask.Type.PRIMARY, vh.primaryView) vh.primaryPropertyTask?.execute(observation) vh.secondaryView.text = "" - vh.secondaryPropertyTask = PropertyTask(context, PropertyTask.Type.SECONDARY, vh.secondaryView) + vh.secondaryPropertyTask = PropertyTask(eventLocalDataSource, PropertyTask.Type.SECONDARY, vh.secondaryView) vh.secondaryPropertyTask?.execute(observation) vh.userView.text = "" @@ -190,7 +212,7 @@ class ObservationListAdapter( vh.directionsButton.setOnClickListener { getDirections(observation) } } catch (e: SQLException) { - e.printStackTrace() + Log.e(LOG_NAME, "Error reading observation from database", e) } } @@ -208,10 +230,12 @@ class ObservationListAdapter( vh.importantView.visibility = if (isImportant) View.VISIBLE else View.GONE if (isImportant) { try { - val user = UserHelper.getInstance(context).read(important!!.userId) - vh.importantOverline.text = String.format("FLAGGED BY %s", user.displayName.uppercase(Locale.getDefault())) + important?.userId?.let { + val user = userLocalDataSource.read(it) + vh.importantOverline.text = String.format("FLAGGED BY %s", user?.displayName?.uppercase(Locale.getDefault())) + } } catch (e: UserException) { - e.printStackTrace() + Log.e(LOG_NAME, "Error reading important user", e) } vh.importantDescription.text = important!!.description } @@ -231,26 +255,27 @@ class ObservationListAdapter( } private fun toggleFavorite(observation: Observation, vh: ObservationViewHolder) { - val observationHelper = ObservationHelper.getInstance(context) val isFavorite = isFavorite(observation) - try { - if (isFavorite) { - observationHelper.unfavoriteObservation(observation, currentUser) - } else { - observationHelper.favoriteObservation(observation, currentUser) + userLocalDataSource.readCurrentUser()?.let { user -> + try { + if (isFavorite) { + observationLocalDataSource.unfavoriteObservation(observation, user) + } else { + observationLocalDataSource.favoriteObservation(observation, user) + } + setFavoriteImage(observation.favorites, vh, isFavorite) + } catch (e: ObservationException) { + Log.e(LOG_NAME, "Could not unfavorite observation", e) } - setFavoriteImage(observation.favorites, vh, isFavorite) - } catch (e: ObservationException) { - Log.e(LOG_NAME, "Could not unfavorite observation", e) } } private fun isFavorite(observation: Observation): Boolean { var isFavorite = false try { - currentUser = UserHelper.getInstance(context).readCurrentUser() - if (currentUser != null) { - val favorite = observation.favoritesMap[currentUser!!.remoteId] + userLocalDataSource.readCurrentUser()?.let { user -> + currentUser = user + val favorite = observation.favoritesMap[user.remoteId] isFavorite = favorite != null && favorite.isFavorite } } catch (e: UserException) { @@ -271,13 +296,11 @@ class ObservationListAdapter( private val reference: WeakReference = WeakReference(textView) override fun doInBackground(vararg observations: Observation?): User? { - var user: User? = null - try { - user = UserHelper.getInstance(context).read(observations[0]?.userId) - } catch (e: UserException) { - Log.e(LOG_NAME, "Could not get user", e) - } - return user + return try { + observations.firstOrNull()?.userId?.let { userId -> + userLocalDataSource.read(userId) + } + } catch (e: Exception) { null } } override fun onPostExecute(u: User?) { @@ -294,15 +317,15 @@ class ObservationListAdapter( } } - private class PropertyTask(private val context: Context, private val type: Type, textView: TextView) : AsyncTask() { + private class PropertyTask(private val eventLocalDataSource: EventLocalDataSource, private val type: Type, textView: TextView) : AsyncTask() { enum class Type { PRIMARY, SECONDARY } private val reference: WeakReference = WeakReference(textView) override fun doInBackground(vararg observations: Observation?): ObservationProperty? { val field = observations[0]?.forms?.firstOrNull()?.let { observationForm -> - val form = EventHelper.getInstance(context).getForm(observationForm.formId) - val fieldName = if (type == Type.PRIMARY) form.primaryFeedField else form.secondaryFeedField + val form = eventLocalDataSource.getForm(observationForm.formId) + val fieldName = if (type == Type.PRIMARY) form?.primaryFeedField else form?.secondaryFeedField observationForm.properties.find { it.key == fieldName } } diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedFragment.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedFragment.kt index 0340e70e4..283adf4ce 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedFragment.kt @@ -18,10 +18,13 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.LandingViewModel import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource import mil.nga.giat.mage.filter.LocationFilterActivity import mil.nga.giat.mage.profile.ProfileActivity -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.user.User +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.database.model.user.User import mil.nga.giat.mage.utils.googleMapsUri import javax.inject.Inject @@ -30,6 +33,10 @@ class UserFeedFragment : Fragment() { @Inject lateinit var application: Application + @Inject lateinit var teamLocalDataSource: TeamLocalDataSource + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + @Inject lateinit var locationLocalDataSource: LocationLocalDataSource + private val viewModel: UserFeedViewModel by activityViewModels() private val landingViewModel: LandingViewModel by activityViewModels() @@ -57,7 +64,10 @@ class UserFeedFragment : Fragment() { viewModel.userFeedState.observe(viewLifecycleOwner) { userFeedState: UserFeedState -> recyclerView.adapter = UserListAdapter( requireContext(), - userFeedState, + teamLocalDataSource = teamLocalDataSource, + eventLocalDataSource = eventLocalDataSource, + locationLocalDataSource = locationLocalDataSource, + userFeedState = userFeedState, userAction = { onUserAction(it) }, userClickListener = { onUserClick(it) } ) diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedViewModel.kt index 786c7084a..7e067f03a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserFeedViewModel.kt @@ -13,12 +13,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.location.LocationRepository -import mil.nga.giat.mage.sdk.datastore.DaoStore -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.data.repository.location.LocationRepository +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.sdk.event.ILocationEventListener import mil.nga.giat.mage.sdk.exceptions.UserException import java.sql.SQLException @@ -30,15 +29,14 @@ data class UserFeedState(val cursor: Cursor, val query: PreparedQuery, @HiltViewModel class UserFeedViewModel @Inject constructor( - val application: Application, - val sharedPreferences: SharedPreferences, - val locationRepository: LocationRepository + private val application: Application, + private val sharedPreferences: SharedPreferences, + private val locationDao: Dao, + private val locationRepository: LocationRepository, + private val locationLocalDataSource: LocationLocalDataSource, + private val userLocalDataSource: UserLocalDataSource ): ViewModel() { - private val userHelper = UserHelper.getInstance(application) - private val locationHelper = LocationHelper.getInstance(application) - private var locationDao: Dao = DaoStore.getInstance(application).locationDao - private val _refreshState = MutableLiveData() val refreshState: LiveData = _refreshState @@ -66,14 +64,14 @@ class UserFeedViewModel @Inject constructor( init { filter.value = getTimeFilterId() - locationHelper.addListener(locationListener) + locationLocalDataSource.addListener(locationListener) sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesChangeListener) } override fun onCleared() { super.onCleared() - locationHelper.removeListener(locationListener) + locationLocalDataSource.removeListener(locationListener) sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferencesChangeListener) } @@ -130,10 +128,7 @@ class UserFeedViewModel @Inject constructor( } } - var currentUser: User? = null - try { - currentUser = userHelper.readCurrentUser() - } catch (ignore: UserException) { } + val currentUser = userLocalDataSource.readCurrentUser() val queryBuilder = locationDao.queryBuilder() val where = queryBuilder.where().gt("timestamp", calendar.time) diff --git a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserListAdapter.kt b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserListAdapter.kt index b395f45de..835027b5d 100644 --- a/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserListAdapter.kt +++ b/mage/src/main/java/mil/nga/giat/mage/newsfeed/UserListAdapter.kt @@ -21,12 +21,12 @@ import mil.nga.giat.mage.R import mil.nga.giat.mage.coordinate.CoordinateFormatter import mil.nga.giat.mage.glide.GlideApp import mil.nga.giat.mage.glide.model.Avatar.Companion.forUser -import mil.nga.giat.mage.sdk.datastore.location.Location -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper -import mil.nga.giat.mage.sdk.datastore.user.EventHelper -import mil.nga.giat.mage.sdk.datastore.user.Team -import mil.nga.giat.mage.sdk.datastore.user.TeamHelper -import mil.nga.giat.mage.sdk.datastore.user.User +import mil.nga.giat.mage.database.model.location.Location +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import mil.nga.giat.mage.database.model.team.Team +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource +import mil.nga.giat.mage.database.model.user.User import mil.nga.giat.mage.utils.DateFormatFactory import mil.nga.sf.util.GeometryUtils import org.apache.commons.lang3.StringUtils @@ -44,6 +44,9 @@ sealed class UserAction { class UserListAdapter( private val context: Context, userFeedState: UserFeedState, + private val teamLocalDataSource: TeamLocalDataSource, + private val eventLocalDataSource: EventLocalDataSource, + private val locationLocalDataSource: LocationLocalDataSource, private val userAction: (UserAction) -> Unit, private val userClickListener: (User) -> Unit ) : RecyclerView.Adapter() { @@ -52,8 +55,6 @@ class UserListAdapter( private var cursor: Cursor = userFeedState.cursor private val query: PreparedQuery = userFeedState.query private val filterText: String = userFeedState.filterText - private val teamHelper = TeamHelper.getInstance(context) - private val eventHelper = EventHelper.getInstance(context) private class PersonViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val cardView: View = view.findViewById(R.id.card) @@ -119,27 +120,29 @@ class UserListAdapter( vh.dateView.text = timeText - val userTeams: MutableCollection = teamHelper.getTeamsByUser(user) - val event = eventHelper.currentEvent - val eventTeams = teamHelper.getTeamsByEvent(event) - userTeams.retainAll(eventTeams.toSet()) - val teamNames = Collections2.transform(userTeams) { team: Team -> team.name } - vh.teamsView.text = StringUtils.join(teamNames, ", ") - var locationIconColor = ContextCompat.getColor(context, R.color.primary_icon) - val locations = LocationHelper.getInstance(context).getUserLocations(user.id, event.id, 1, true) - locations?.first()?.let { location: Location -> - if (location.propertiesMap["accuracy_type"]?.value == "COARSE") { - locationIconColor = ContextCompat.getColor(context, R.color.md_amber_700) + val userTeams = teamLocalDataSource.getTeamsByUser(user).toMutableList() + eventLocalDataSource.currentEvent?.let { event -> + val eventTeams = teamLocalDataSource.getTeamsByEvent(event) + userTeams.retainAll(eventTeams.toSet()) + + val locations = locationLocalDataSource.getUserLocations(user.id, event.id, 1, true) + locations.firstOrNull()?.let { location: Location -> + if (location.propertiesMap["accuracy_type"]?.value == "COARSE") { + locationIconColor = ContextCompat.getColor(context, R.color.md_amber_700) + } + + val point = GeometryUtils.getCentroid(location.geometry) + val coordinates = CoordinateFormatter(context).format(LatLng(point.y, point.x)) + vh.location.text = coordinates + vh.location.setTextColor(locationIconColor) + vh.location.setOnClickListener { userAction(UserAction.Coordinates(coordinates)) } + vh.directions.setOnClickListener { userAction(UserAction.Directions(user, location)) } } - val point = GeometryUtils.getCentroid(location.geometry) - val coordinates = CoordinateFormatter(context).format(LatLng(point.y, point.x)) - vh.location.text = coordinates - vh.location.setTextColor(locationIconColor) - vh.location.setOnClickListener { userAction(UserAction.Coordinates(coordinates)) } - vh.directions.setOnClickListener { userAction(UserAction.Directions(user, location)) } } + val teamNames = Collections2.transform(userTeams) { team: Team -> team.name } + vh.teamsView.text = StringUtils.join(teamNames, ", ") val locationIcon = DrawableCompat.wrap(ContextCompat.getDrawable(context, R.drawable.ic_my_location_white_24dp)!!) DrawableCompat.setTint(locationIcon, locationIconColor) diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/ImportantDialog.java b/mage/src/main/java/mil/nga/giat/mage/observation/ImportantDialog.java index ac62d3437..53543f8a0 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/ImportantDialog.java +++ b/mage/src/main/java/mil/nga/giat/mage/observation/ImportantDialog.java @@ -1,7 +1,6 @@ package mil.nga.giat.mage.observation; import android.app.Dialog; -import android.content.DialogInterface; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -12,7 +11,6 @@ import androidx.appcompat.app.AppCompatDialogFragment; import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationImportant; /** * Created by wnewman on 8/22/16. diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationLocation.java b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationLocation.java index b6ee99269..3ef74139f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationLocation.java +++ b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationLocation.java @@ -16,8 +16,8 @@ import java.util.List; import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; -import mil.nga.giat.mage.sdk.utils.GeometryUtility; +import mil.nga.giat.mage.database.model.observation.Observation; +import mil.nga.giat.mage.sdk.utils.GeometryUtilityKt; import mil.nga.proj.ProjectionConstants; import mil.nga.sf.CompoundCurve; import mil.nga.sf.Geometry; @@ -186,7 +186,7 @@ public ObservationLocation(ObservationLocation location) { public ObservationLocation(Parcel in) { byte[] geometryBytes = new byte[in.readInt()]; in.readByteArray(geometryBytes); - geometry = GeometryUtility.toGeometry(geometryBytes); + geometry = GeometryUtilityKt.toGeometry(geometryBytes); accuracy = (Float) in.readValue(Float.class.getClassLoader()); provider = in.readString(); time = in.readLong(); @@ -495,7 +495,7 @@ public int describeContents() { */ @Override public void writeToParcel(Parcel out, int flags) { - byte[] geometryBytes = GeometryUtility.toGeometryBytes(geometry); + byte[] geometryBytes = GeometryUtilityKt.toBytes(geometry); out.writeInt(geometryBytes.length); out.writeByteArray(geometryBytes); out.writeValue(accuracy); diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationNotificationListener.java b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationNotificationListener.java index d6a4e73d0..56cc98b48 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationNotificationListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationNotificationListener.java @@ -13,7 +13,7 @@ import mil.nga.giat.mage.LandingActivity; import mil.nga.giat.mage.MageApplication; import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; +import mil.nga.giat.mage.database.model.observation.Observation; import mil.nga.giat.mage.sdk.event.IObservationEventListener; /** @@ -50,7 +50,7 @@ public void onObservationCreated(Collection observations, Boolean s if (notificationsEnabled) { for (Observation obs : observations) { if (obs.getRemoteId() != null) { - remoteObservations = Boolean.TRUE; + remoteObservations = true; break; } } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationShareTask.java b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationShareTask.java deleted file mode 100644 index 8118936cc..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationShareTask.java +++ /dev/null @@ -1,236 +0,0 @@ -package mil.nga.giat.mage.observation; - -import android.app.Activity; -import android.app.ProgressDialog; -import android.content.Intent; -import android.net.Uri; -import android.os.AsyncTask; -import androidx.core.content.FileProvider; -import android.text.Html; -import android.text.Spanned; -import android.util.Log; -import android.widget.Toast; - -import com.google.common.io.Closeables; -import com.google.common.io.Files; -import com.google.gson.JsonObject; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; - -import mil.nga.giat.mage.sdk.datastore.observation.Attachment; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationProperty; -import mil.nga.giat.mage.sdk.http.resource.ObservationResource; -import okhttp3.ResponseBody; - -/** - * Created by wnewman on 8/27/16. - */ -public class ObservationShareTask extends AsyncTask> { - - public interface OnShareableListener { - void onShareable(ArrayList uris); - } - - private static final String LOG_NAME = ObservationShareTask.class.getName(); - - Activity activity; - OnShareableListener shareableListener; - ProgressDialog progressDialog; - Observation observation; - - public ObservationShareTask(Activity activity, Observation observation) { - this.activity = activity; - this.observation = observation; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - - if (observation.getAttachments().size() > 0) { - progressDialog = new ProgressDialog(activity); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setCancelable(false); - progressDialog.setMessage("Attaching file 1 of " + observation.getAttachments().size()); - progressDialog.show(); - } - } - - @Override - protected ArrayList doInBackground(Void... params) { - Attachment[] attachments = observation.getAttachments().toArray(new Attachment[observation.getAttachments().size()]); - - ArrayList uris = new ArrayList<>(); - - InputStream is = null; - OutputStream os = null; - - for (int i = 0; i < attachments.length; i++) { - publishProgress(i, 0); - - Attachment attachment = attachments[i]; - - if (attachment.getLocalPath() != null && new File(attachment.getLocalPath()).exists()) { - Uri uri = Uri.fromFile(new File(attachment.getLocalPath())); - uris.add(uri); - publishProgress(i, 100); - continue; - } - - File cacheDir = new File(activity.getCacheDir(), "attachments"); - cacheDir.mkdirs(); - - String extension = Files.getFileExtension(attachment.getName()); - File file = new File(cacheDir, attachment.getId().toString() + "." + extension); - Uri uri = FileProvider.getUriForFile(activity, "mil.nga.giat.mage.fileprovider", file); - if (file.exists()) { - publishProgress(i, 100); - uris.add(uri); - continue; - } - - try { - ObservationResource observationResource = new ObservationResource(activity); - ResponseBody response = observationResource.getAttachment(attachment); - - Long contentLength = response.contentLength(); - - os = new FileOutputStream(file); - - byte data[] = new byte[1024]; - - Long total = 0l; - int count; - is = response.byteStream(); - while ((count = is.read(data)) != -1) { - total += count; - publishProgress(i, ((Double) (100.0 * (total.doubleValue() / contentLength.doubleValue()))).intValue()); - os.write(data, 0, count); - } - - uris.add(uri); - } catch (Exception e) { - Log.e(LOG_NAME, "Problem downloading attachment.", e); - } finally { - Closeables.closeQuietly(is); - - if (os != null) { - try { - os.close();; - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - return uris; - } - - protected void onProgressUpdate(Integer... progress) { - Integer number = progress[0] + 1; - Integer percentage = progress[1]; - - progressDialog.setProgress(percentage); - progressDialog.setMessage("Attaching file " + number + " of " + observation.getAttachments().size()); - } - - @Override - protected void onPostExecute(ArrayList uris) { - if (progressDialog != null) { - progressDialog.dismiss(); - } - - if (uris.size() != observation.getAttachments().size()) { - Toast toast = Toast.makeText(activity, "One or more attachments failed to attach", Toast.LENGTH_LONG); - toast.show(); - } - - Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); - intent.setType("text/plain"); - intent.putExtra(Intent.EXTRA_SUBJECT, observation.getEvent().getName() + " MAGE Observation"); - intent.putExtra(Intent.EXTRA_TEXT, observationText(observation)); - intent.putExtra(Intent.EXTRA_STREAM, uris); - activity.startActivity(Intent.createChooser(intent, "Share Observation")); - } - - - private Spanned observationText(Observation observation) { - // TODO when we turn this back on, make it work for multiple forms - -// JsonObject formJson = observation.getEvent().getForm(); -// Map nameToField = new TreeMap<>(); -// JsonArray dynamicFormFields = formJson.get("fields").getAsJsonArray(); -// for (int i = 0; i < dynamicFormFields.size(); i++) { -// JsonObject field = dynamicFormFields.get(i).getAsJsonObject(); -// String name = field.get("name").getAsString(); -// nameToField.put(name, field); -// } - - StringBuilder builder = new StringBuilder(); - -// try { -// User user = UserHelper.getInstance(activity).read(observation.getUserId()); -// builder.append("Created by:
") -// .append(user.getDisplayName()) -// .append("

"); -// } catch (UserException e) { -// Log.e(LOG_NAME, "Error reading user with id: " + observation.getUserId(), e); -// } -// -// builder.append("Date:
") -// .append(observation.getTimestamp()) -// .append("

"); -// -// Point point = GeometryUtils.getCentroid(observation.getGeometry()); -// builder.append("Latitude, Longitude:
") -// .append(point.getY()).append(", ").append(point.getX()) -// .append("

"); -// -// ObservationProperty type = observation.getPropertiesMap().get("type"); -// builder.append(propertyText(type, nameToField.get(type.getKey()))); -// -// JsonElement variantJson = formJson.get("variantField"); -// String variantField = null; -// if (variantJson != null) { -// variantField = variantJson.getAsString(); -// ObservationProperty variantProperty = observation.getPropertiesMap().get(variantField); -// if (variantProperty != null) { -// JsonObject field = nameToField.get(variantProperty.getKey()); -// builder.append(propertyText(variantProperty, field)); -// } -// } -// -// for (ObservationProperty property : observation.getProperties()) { -// JsonObject field = nameToField.get(property.getKey()); -// if (field == null || "type".equals(property.getKey()) || "timestamp".equals(property.getKey()) || property.getKey().equals(variantField)) { -// continue; -// } -// -// Serializable value = property.getValue(); -// if (value == null || (value instanceof String && (StringUtils.isEmpty((String) value))) ||(value instanceof Collection && ((Collection) value).isEmpty())) { -// continue; -// } -// -// JsonElement archivedJson = field.get("archived"); -// if (archivedJson != null && archivedJson.getAsBoolean()) { -// continue; -// } -// -// builder.append(propertyText(property, field)); -// } - - return Html.fromHtml(builder.toString()); - } - - private String propertyText(ObservationProperty property, JsonObject field) { - String title = field.get("title").getAsString(); - return "" + title + ":
" + property.getValue() + "

"; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationState.kt b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationState.kt index 688033493..d4f8629fb 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/ObservationState.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/ObservationState.kt @@ -7,7 +7,7 @@ import mil.nga.giat.mage.form.Form import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.field.DateFieldState import mil.nga.giat.mage.form.field.GeometryFieldState -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import java.util.* enum class ObservationPermission { @@ -16,18 +16,18 @@ enum class ObservationPermission { // TODO multi-form, this state class has gotten rather big class ObservationState( - id: Long? = null, - status: ObservationStatusState, - val definition: ObservationDefinition, - val timestampFieldState: DateFieldState, - val geometryFieldState: GeometryFieldState, - val userDisplayName: String?, - val permissions: Set = emptySet(), - forms: List, - attachments: Collection = emptyList(), - important: ObservationImportantState? = null, - favorite: Boolean = false, - favorites: Int = 0 + id: Long? = null, + status: ObservationStatusState, + val definition: ObservationDefinition, + val timestampFieldState: DateFieldState, + val geometryFieldState: GeometryFieldState, + val userDisplayName: String?, + val permissions: Set = emptySet(), + forms: List, + attachments: Collection = emptyList(), + important: ObservationImportantState? = null, + favorite: Boolean = false, + favorites: Int = 0 ) { val id by mutableStateOf(id) val status = mutableStateOf(status) diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentGallery.java b/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentGallery.java index 31a15be84..f511cd4b1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentGallery.java +++ b/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentGallery.java @@ -19,7 +19,7 @@ import mil.nga.giat.mage.R; import mil.nga.giat.mage.glide.GlideApp; import mil.nga.giat.mage.glide.transform.VideoOverlayTransformation; -import mil.nga.giat.mage.sdk.datastore.observation.Attachment; +import mil.nga.giat.mage.database.model.observation.Attachment; /** * Created by wnewman on 5/11/15. diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentViewModel.kt b/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentViewModel.kt index 4a29b269e..f584fdbf7 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentViewModel.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/attachment/AttachmentViewModel.kt @@ -15,11 +15,12 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.observation.AttachmentRepository -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.observation.AttachmentHelper +import mil.nga.giat.mage.data.repository.observation.AttachmentRepository +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource import mil.nga.giat.mage.sdk.utils.MediaUtility import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.apache.commons.lang3.StringUtils import java.io.* import javax.inject.Inject @@ -38,9 +39,9 @@ data class Shareable(val type: Type, val uri: Uri, val contentType: String) { class AttachmentViewModel @Inject constructor( val application: Application, val preferences: SharedPreferences, - private val attachmentRepository: AttachmentRepository + private val attachmentRepository: AttachmentRepository, + private val attachmentLocalDataSource: AttachmentLocalDataSource ): ViewModel() { - private val attachmentHelper: AttachmentHelper = AttachmentHelper.getInstance(application) private val _attachmentUri = MutableLiveData() val attachmentUri: LiveData = _attachmentUri @@ -70,7 +71,7 @@ class AttachmentViewModel @Inject constructor( fun setAttachment(id: Long) { viewModelScope.launch(Dispatchers.IO) { - attachment = attachmentHelper.read(id) + attachment = attachmentLocalDataSource.read(id) attachment?.let { attachment -> val contentType = if (StringUtils.isBlank(attachment.contentType) || "application/octet-stream".equals(attachment.contentType, ignoreCase = true)) { var name: String? = attachment.name @@ -91,7 +92,7 @@ class AttachmentViewModel @Inject constructor( if (attachment.localPath != null) { AttachmentState.MediaState(Uri.fromFile(File(attachment.localPath)), contentType) } else { - val url = HttpUrl.parse(attachment.url) + val url = attachment.url.toHttpUrlOrNull() ?.newBuilder() ?.setQueryParameter("access_token", getToken()) ?.toString() @@ -103,7 +104,7 @@ class AttachmentViewModel @Inject constructor( if (attachment.localPath != null) { AttachmentState.OtherState(Uri.fromFile(File(attachment.localPath)), contentType) } else { - val url = HttpUrl.parse(attachment.url) + val url = attachment.url.toHttpUrlOrNull() ?.newBuilder() ?.setQueryParameter("access_token", getToken()) ?.toString() diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/edit/FormPickerBottomSheetFragment.kt b/mage/src/main/java/mil/nga/giat/mage/observation/edit/FormPickerBottomSheetFragment.kt index b5dab9f43..cc48a49d2 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/edit/FormPickerBottomSheetFragment.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/edit/FormPickerBottomSheetFragment.kt @@ -20,7 +20,8 @@ import mil.nga.giat.mage.databinding.ViewFormPickerItemBinding import mil.nga.giat.mage.form.Form import mil.nga.giat.mage.form.FormViewModel import mil.nga.giat.mage.network.gson.asJsonObjectOrNull -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource +import javax.inject.Inject @AndroidEntryPoint class FormPickerBottomSheetFragment: BottomSheetDialogFragment() { @@ -29,6 +30,8 @@ class FormPickerBottomSheetFragment: BottomSheetDialogFragment() { fun onFormPicked(form: Form) } + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + data class FormState(val form: Form, val disabled: Boolean) var formPickerListener: OnFormClickListener? = null @@ -56,7 +59,7 @@ class FormPickerBottomSheetFragment: BottomSheetDialogFragment() { recyclerView.layoutManager = LinearLayoutManager(context) // TODO get from ViewModel - val jsonForms = EventHelper.getInstance(context).currentEvent.forms + val jsonForms = eventLocalDataSource.currentEvent?.forms ?: emptyList() val forms = jsonForms .asSequence() diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditActivity.kt b/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditActivity.kt index bd28360ea..13bf4b7b9 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditActivity.kt @@ -2,9 +2,9 @@ package mil.nga.giat.mage.observation.edit import android.Manifest import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle +import android.os.Environment import android.provider.Settings import android.util.Log import android.webkit.MimeTypeMap @@ -15,7 +15,6 @@ import androidx.activity.result.launch import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.lifecycle.ViewModelProvider import com.google.android.gms.maps.model.LatLng @@ -23,51 +22,62 @@ import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.R import mil.nga.giat.mage.compat.server5.observation.edit.FormViewModel_server5 -import mil.nga.giat.mage.form.* +import mil.nga.giat.mage.form.AttachmentFormField +import mil.nga.giat.mage.form.AttachmentType +import mil.nga.giat.mage.form.ChoiceFormField +import mil.nga.giat.mage.form.Form +import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.FormViewModel import mil.nga.giat.mage.form.edit.dialog.DateFieldDialog import mil.nga.giat.mage.form.edit.dialog.FormReorderDialog import mil.nga.giat.mage.form.edit.dialog.GeometryFieldDialog import mil.nga.giat.mage.form.edit.dialog.SelectFieldDialog import mil.nga.giat.mage.form.edit.dialog.SelectFieldDialog.Companion.newInstance -import mil.nga.giat.mage.form.field.* -import mil.nga.giat.mage.network.gson.observation.ObservationTypeAdapter +import mil.nga.giat.mage.form.field.DateFieldState +import mil.nga.giat.mage.form.field.FieldState +import mil.nga.giat.mage.form.field.FieldValue +import mil.nga.giat.mage.form.field.GeometryFieldState +import mil.nga.giat.mage.form.field.Media +import mil.nga.giat.mage.form.field.MultiSelectFieldState +import mil.nga.giat.mage.form.field.SelectFieldState +import mil.nga.giat.mage.network.observation.ObservationTypeAdapter import mil.nga.giat.mage.observation.ObservationLocation import mil.nga.giat.mage.observation.attachment.AttachmentViewActivity import mil.nga.giat.mage.observation.edit.FormPickerBottomSheetFragment.OnFormClickListener import mil.nga.giat.mage.sdk.Compatibility.Companion.isServerVersion5 -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.user.EventHelper +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource import mil.nga.giat.mage.sdk.utils.MediaUtility import mil.nga.sf.Point import java.io.File import java.io.IOException -import java.util.* - +import java.util.Date +import java.util.UUID +import javax.inject.Inject + +sealed class PermissionRequest( + val permission: String, + val deniedTitleResourceId: Int, + val deniedMessageResourceId: Int) { + class Image: PermissionRequest( + Manifest.permission.CAMERA, + R.string.camera_access_title, + R.string.camera_access_message + ) + class Video: PermissionRequest( + Manifest.permission.CAMERA, + R.string.camera_access_title, + R.string.camera_access_message + ) + class Audio: PermissionRequest( + Manifest.permission.RECORD_AUDIO, + R.string.audio_access_title, + R.string.audio_access_message + ) +} @AndroidEntryPoint open class ObservationEditActivity : AppCompatActivity() { - companion object { - private val LOG_NAME = ObservationEditActivity::class.java.name - - const val OBSERVATION_ID = "OBSERVATION_ID" - const val LOCATION = "LOCATION" - const val INITIAL_LOCATION = "INITIAL_LOCATION" - const val INITIAL_ZOOM = "INITIAL_ZOOM" - - private const val DRAFT_OBSERVATION_ID = "DRAFT_OBSERVATION_ID" - private const val DRAFT_OBSERVATION_JSON = "DRAFT_OBSERVATION_JSON" - private const val CURRENT_MEDIA_PATH = "CURRENT_MEDIA_PATH" - private const val ATTACHMENT_MEDIA_ACTION = "ATTACHMENT_MEDIA_ACTION" - - private const val PERMISSIONS_REQUEST_CAMERA = 100 - private const val PERMISSIONS_REQUEST_VIDEO = 200 - private const val PERMISSIONS_REQUEST_AUDIO = 300 - private const val PERMISSIONS_REQUEST_STORAGE = 400 - - private const val NEW_OBSERVATION = -1L - } - protected lateinit var viewModel: FormViewModel private var currentMediaPath: String? = null @@ -76,6 +86,26 @@ open class ObservationEditActivity : AppCompatActivity() { private var defaultMapLatLng = LatLng(0.0, 0.0) private var defaultMapZoom: Float = 0f + @Inject lateinit var eventLocalDataSource: EventLocalDataSource + + private val requestImagePermission = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { result -> + this.onPermission(PermissionRequest.Image(), result) + } + + private val requestVideoPermission = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { result -> + this.onPermission(PermissionRequest.Video(), result) + } + + private val requestAudioPermission = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { result -> + this.onPermission(PermissionRequest.Audio(), result) + } + private val getAudio = registerForActivityResult( CaptureAudio() ) { uri: Uri? -> @@ -177,7 +207,7 @@ open class ObservationEditActivity : AppCompatActivity() { val draftObservation = savedInstanceState.getString(DRAFT_OBSERVATION_JSON)!! val observation = ObservationTypeAdapter().fromJson(draftObservation) - observation.event = EventHelper.getInstance(applicationContext).currentEvent + observation.event = eventLocalDataSource.currentEvent if (savedInstanceState.containsKey(DRAFT_OBSERVATION_ID)) { observation.id = savedInstanceState.getLong(DRAFT_OBSERVATION_ID) } @@ -187,94 +217,6 @@ open class ObservationEditActivity : AppCompatActivity() { currentMediaPath = savedInstanceState.getString(CURRENT_MEDIA_PATH) } - private fun onUris(uris: List) { - val mediaAction = attachmentMediaAction - - uris.forEach { uri -> - try { - val file = MediaUtility.copyMediaFromUri(applicationContext, uri) - val attachment = Attachment() - attachment.action = Media.ATTACHMENT_ADD_ACTION - attachment.localPath = file.absolutePath - attachment.name = file.name - attachment.contentType = contentResolver.getType(uri) - attachment.size = file.length() - viewModel.addAttachment(attachment, mediaAction) - } catch (e: IOException) { - Log.e(LOG_NAME, "Error copying document to local storage", e) - - val fileName = MediaUtility.getDisplayName(applicationContext, uri) - val displayName = if (fileName.length > 12) "${fileName.substring(0, 12)}..." else fileName - val message = String.format(resources.getString(R.string.observation_edit_invalid_attachment), displayName) - Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show() - } - } - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - - when (requestCode) { - PERMISSIONS_REQUEST_CAMERA, PERMISSIONS_REQUEST_VIDEO -> { - val grants: MutableMap = HashMap() - grants[Manifest.permission.CAMERA] = PackageManager.PERMISSION_GRANTED - grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] = PackageManager.PERMISSION_GRANTED - for (i in grantResults.indices) { - grants[permissions[i]] = grantResults[i] - } - - if (grants[Manifest.permission.CAMERA] == PackageManager.PERMISSION_GRANTED && - grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] == PackageManager.PERMISSION_GRANTED) { - if (requestCode == PERMISSIONS_REQUEST_CAMERA) { - launchCameraIntent() - } else { - launchVideoIntent() - } - } else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) && grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] == PackageManager.PERMISSION_GRANTED || - !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) && grants[Manifest.permission.CAMERA] == PackageManager.PERMISSION_GRANTED || - !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - // User denied camera or storage with never ask again. Since they will get here - // by clicking the camera button give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - resources.getString(R.string.camera_access_title), - resources.getString(R.string.camera_access_message)) - } - } - PERMISSIONS_REQUEST_AUDIO -> { - val grants: MutableMap = HashMap() - grants[Manifest.permission.RECORD_AUDIO] = PackageManager.PERMISSION_GRANTED - grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] = PackageManager.PERMISSION_GRANTED - if (grants[Manifest.permission.RECORD_AUDIO] == PackageManager.PERMISSION_GRANTED && grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] == PackageManager.PERMISSION_GRANTED) { - launchAudioIntent() - } else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO) && grants[Manifest.permission.WRITE_EXTERNAL_STORAGE] == PackageManager.PERMISSION_GRANTED || - !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) && grants[Manifest.permission.RECORD_AUDIO] == PackageManager.PERMISSION_GRANTED || - !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO) && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - // User denied camera or storage with never ask again. Since they will get here - // by clicking the camera button give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - resources.getString(R.string.camera_access_title), - resources.getString(R.string.camera_access_message)) - } - } - PERMISSIONS_REQUEST_STORAGE -> { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - launchGalleryIntent(attachmentMediaAction) - } else { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - // User denied storage with never ask again. Since they will get here - // by clicking the gallery button give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - resources.getString(R.string.gallery_access_title), - resources.getString(R.string.gallery_access_message)) - } - } - } - } - } - private fun showDisabledPermissionsDialog(title: String, message: String) { AlertDialog.Builder(this) .setTitle(title) @@ -303,68 +245,66 @@ open class ObservationEditActivity : AppCompatActivity() { .show() } - private fun onMediaAction(mediaAction: MediaAction) { - attachmentMediaAction = mediaAction - - when (mediaAction.type) { - MediaActionType.PHOTO -> onCameraAction() - MediaActionType.VIDEO -> onVideoAction() - MediaActionType.GALLERY -> onGalleryAction(mediaAction) - MediaActionType.VOICE -> onVoiceAction() - MediaActionType.FILE -> onFileAction() + private fun onPermission(request: PermissionRequest, result: Boolean) { + if (result) { + when (request) { + is PermissionRequest.Image -> launchCameraIntent() + is PermissionRequest.Video -> launchVideoIntent() + is PermissionRequest.Audio -> launchAudioIntent() + } + } else if (!ActivityCompat.shouldShowRequestPermissionRationale(this, request.permission)) { + showDisabledPermissionsDialog( + resources.getString(request.deniedTitleResourceId), + resources.getString(request.deniedMessageResourceId)) } } - private fun onCameraAction() { - if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_CAMERA) - } else { - launchCameraIntent() - } - } + private fun onUris(uris: List) { + val mediaAction = attachmentMediaAction - private fun onVideoAction() { - if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_VIDEO) - } else { - launchVideoIntent() - } - } + uris.forEach { uri -> + try { + val file = MediaUtility.copyMediaFromUri(applicationContext, uri) + val attachment = + Attachment() + attachment.action = Media.ATTACHMENT_ADD_ACTION + attachment.localPath = file.absolutePath + attachment.name = file.name + attachment.contentType = contentResolver.getType(uri) + attachment.size = file.length() + viewModel.addAttachment(attachment, mediaAction) + } catch (e: IOException) { + Log.e(LOG_NAME, "Error copying document to local storage", e) - private fun onGalleryAction(mediaAction: MediaAction) { - if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_STORAGE) - } else { - launchGalleryIntent(mediaAction) + val fileName = MediaUtility.getDisplayName(applicationContext, uri) + val displayName = if (fileName.length > 12) "${fileName.substring(0, 12)}..." else fileName + val message = String.format(resources.getString(R.string.observation_edit_invalid_attachment), displayName) + Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_LONG).show() + } } } - private fun onVoiceAction() { - if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_AUDIO) - } else { - launchAudioIntent() - } - } + private fun onMediaAction(mediaAction: MediaAction) { + attachmentMediaAction = mediaAction - private fun onFileAction() { - if (ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_STORAGE) - } else { - launchFileIntent() + when (mediaAction.type) { + MediaActionType.PHOTO -> requestImagePermission.launch(Manifest.permission.CAMERA) + MediaActionType.VIDEO -> requestVideoPermission.launch(Manifest.permission.CAMERA) + MediaActionType.VOICE -> requestAudioPermission.launch(Manifest.permission.RECORD_AUDIO) + MediaActionType.GALLERY -> launchGalleryIntent(mediaAction) + MediaActionType.FILE -> launchFileIntent() } } private fun launchCameraIntent() { - val file = MediaUtility.createImageFile() + val file = File( + getExternalFilesDir(Environment.DIRECTORY_PICTURES), + "${UUID.randomUUID()}.jpg", + ) + + val uri = FileProvider.getUriForFile(applicationContext, applicationContext.packageName + ".fileprovider", file) + currentMediaPath = file.absolutePath - val uri = FileProvider.getUriForFile(this, application.packageName + ".fileprovider", file) getImage.launch(uri) } @@ -377,7 +317,8 @@ open class ObservationEditActivity : AppCompatActivity() { MediaUtility.addImageToGallery(applicationContext, uri) val file = MediaUtility.copyMediaFromUri(applicationContext, uri) - val attachment = Attachment() + val attachment = + Attachment() attachment.action = Media.ATTACHMENT_ADD_ACTION attachment.localPath = file.absolutePath attachment.name = file.name @@ -392,7 +333,11 @@ open class ObservationEditActivity : AppCompatActivity() { } private fun launchVideoIntent() { - val file = MediaUtility.createVideoFile() + val file = File( + getExternalFilesDir(Environment.DIRECTORY_MOVIES), + "${UUID.randomUUID()}.mp4" + ) + currentMediaPath = file.absolutePath val uri = FileProvider.getUriForFile(this, application.packageName + ".fileprovider", file) getVideo.launch(uri) @@ -550,4 +495,20 @@ open class ObservationEditActivity : AppCompatActivity() { } dialog.show(supportFragmentManager, "DIALOG_FORM_REORDER") } + + companion object { + private val LOG_NAME = ObservationEditActivity::class.java.name + + private const val DRAFT_OBSERVATION_ID = "DRAFT_OBSERVATION_ID" + private const val DRAFT_OBSERVATION_JSON = "DRAFT_OBSERVATION_JSON" + private const val CURRENT_MEDIA_PATH = "CURRENT_MEDIA_PATH" + private const val ATTACHMENT_MEDIA_ACTION = "ATTACHMENT_MEDIA_ACTION" + + private const val NEW_OBSERVATION = -1L + + const val OBSERVATION_ID = "OBSERVATION_ID" + const val LOCATION = "LOCATION" + const val INITIAL_LOCATION = "INITIAL_LOCATION" + const val INITIAL_ZOOM = "INITIAL_ZOOM" + } } \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditScreen.kt b/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditScreen.kt index 113cf5358..5b0c7d580 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditScreen.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/edit/ObservationEditScreen.kt @@ -1,9 +1,6 @@ package mil.nga.giat.mage.observation.edit -import android.net.Uri import android.os.Parcelable -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -22,6 +19,7 @@ import androidx.compose.ui.unit.dp import kotlinx.android.parcel.Parcelize import kotlinx.coroutines.launch import mil.nga.giat.mage.compat.server5.form.view.AttachmentsViewContentServer5 +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FormState import mil.nga.giat.mage.form.FormViewModel import mil.nga.giat.mage.form.edit.DateEdit @@ -31,7 +29,7 @@ import mil.nga.giat.mage.form.field.* import mil.nga.giat.mage.observation.ObservationState import mil.nga.giat.mage.observation.ObservationValidationResult import mil.nga.giat.mage.sdk.Compatibility.Companion.isServerVersion5 -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import mil.nga.giat.mage.ui.theme.MageTheme enum class AttachmentAction { @@ -51,15 +49,15 @@ data class MediaAction ( @Composable fun ObservationEditScreen( - viewModel: FormViewModel, - onSave: (() -> Unit)? = null, - onCancel: (() -> Unit)? = null, - onAddForm: (() -> Unit)? = null, - onDeleteForm: ((Int) -> Unit)? = null, - onReorderForms: (() -> Unit)? = null, - onFieldClick: ((FieldState<*, *>) -> Unit)? = null, - onAttachmentAction: ((AttachmentAction, Attachment, FieldState<*, *>?) -> Unit)? = null, - onMediaAction: ((MediaAction) -> Unit)? = null + viewModel: FormViewModel, + onSave: (() -> Unit)? = null, + onCancel: (() -> Unit)? = null, + onAddForm: (() -> Unit)? = null, + onDeleteForm: ((Int) -> Unit)? = null, + onReorderForms: (() -> Unit)? = null, + onFieldClick: ((FieldState<*, *>) -> Unit)? = null, + onAttachmentAction: ((AttachmentAction, Attachment, FieldState<*, *>?) -> Unit)? = null, + onMediaAction: ((MediaAction) -> Unit)? = null ) { val observationState by viewModel.observationState.observeAsState() val scope = rememberCoroutineScope() @@ -94,7 +92,8 @@ fun ObservationEditScreen( } ObservationEditContent( - observationState, + event = viewModel.event, + observationState = observationState, listState = listState, onFieldClick = onFieldClick, onMediaAction = onMediaAction, @@ -212,6 +211,7 @@ fun ObservationMediaBar( @Composable fun ObservationEditContent( + event: Event?, observationState: ObservationState?, listState: LazyListState, onFieldClick: ((FieldState<*, *>) -> Unit)? = null, @@ -252,6 +252,7 @@ fun ObservationEditContent( ) { item { ObservationEditHeaderContent( + event = event, timestamp = observationState.timestampFieldState, geometry = observationState.geometryFieldState, formState = forms.getOrNull(0), @@ -302,6 +303,7 @@ fun ObservationEditContent( itemsIndexed(forms) { index, formState -> FormEditContent( + event = event, formState = formState, onFormDelete = { onDeleteForm?.invoke(index, formState) }, onFieldClick = { onFieldClick?.invoke(it) }, @@ -317,6 +319,7 @@ fun ObservationEditContent( @Composable fun ObservationEditHeaderContent( + event: Event?, timestamp: DateFieldState, geometry: GeometryFieldState, formState: FormState? = null, @@ -334,10 +337,11 @@ fun ObservationEditHeaderContent( ) GeometryEdit( - modifier = Modifier.padding(bottom = 16.dp), + event = event, fieldState = geometry, formState = formState, - onClick = onLocationClick + onClick = onLocationClick, + modifier = Modifier.padding(bottom = 16.dp) ) } } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncListener.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncListener.kt index d2b388edc..84ea21e8c 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncListener.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncListener.kt @@ -1,17 +1,17 @@ package mil.nga.giat.mage.observation.sync -import android.content.Context -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.observation.AttachmentHelper +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource import mil.nga.giat.mage.sdk.event.IAttachmentEventListener +import javax.inject.Inject class AttachmentSyncListener( - val context: Context, + attachmentLocalDataSource: AttachmentLocalDataSource, val sync : () -> Unit ): IAttachmentEventListener { init { - AttachmentHelper.getInstance(context).addListener(this) + attachmentLocalDataSource.addListener(this) sync() } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncWorker.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncWorker.kt index bd0807e70..a045c480e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncWorker.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/AttachmentSyncWorker.kt @@ -11,9 +11,8 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import mil.nga.giat.mage.MageApplication import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.observation.AttachmentRepository -import mil.nga.giat.mage.sdk.datastore.observation.AttachmentHelper -import java.io.IOException +import mil.nga.giat.mage.data.repository.observation.AttachmentRepository +import mil.nga.giat.mage.data.datasource.observation.AttachmentLocalDataSource import java.net.HttpURLConnection import java.util.concurrent.TimeUnit @@ -21,6 +20,7 @@ import java.util.concurrent.TimeUnit class AttachmentSyncWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, + private val attachmentLocalDataSource: AttachmentLocalDataSource, private val attachmentRepository: AttachmentRepository ) : CoroutineWorker(context, params) { @@ -53,8 +53,7 @@ class AttachmentSyncWorker @AssistedInject constructor( private suspend fun syncAttachments(): Int { var result = RESULT_SUCCESS_FLAG - val attachmentHelper = AttachmentHelper.getInstance(applicationContext) - for (attachment in attachmentHelper.dirtyAttachments.filter { !it.observation.remoteId.isNullOrEmpty() && it.url.isNullOrEmpty() }) { + for (attachment in attachmentLocalDataSource.dirtyAttachments.filter { !it.observation.remoteId.isNullOrEmpty() && it.url.isNullOrEmpty() }) { val response = attachmentRepository.syncAttachment(attachment) result = if (response.isSuccessful) { Result.success() diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchService.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchService.kt index 9dcc0800b..1987eb3d4 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchService.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchService.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.observation.ObservationRepository +import mil.nga.giat.mage.data.repository.observation.ObservationRepository import javax.inject.Inject @AndroidEntryPoint diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchWorker.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchWorker.kt index 59b0ef5cd..80876da9d 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchWorker.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationFetchWorker.kt @@ -4,24 +4,24 @@ import android.content.Context import android.util.Log import androidx.hilt.work.HiltWorker import androidx.work.* -import androidx.work.PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS +import androidx.work.PeriodicWorkRequest.Companion.MIN_PERIODIC_INTERVAL_MILLIS import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import mil.nga.giat.mage.data.observation.ObservationRepository -import mil.nga.giat.mage.sdk.utils.UserUtility +import mil.nga.giat.mage.data.repository.observation.ObservationRepository +import mil.nga.giat.mage.di.TokenProvider import java.util.* import java.util.concurrent.TimeUnit @HiltWorker class ObservationFetchWorker @AssistedInject constructor( - @Assisted context: Context, - @Assisted params: WorkerParameters, - private val observationRepository: ObservationRepository + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val observationRepository: ObservationRepository, + private val tokenProvider: TokenProvider ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { - // Check token - if (UserUtility.getInstance(applicationContext).isTokenExpired) { + if (tokenProvider.isExpired()) { Log.d(LOG_NAME, "Token expired, turn off observation fetch worker.") return Result.failure(); } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncListener.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncListener.kt index e53719eef..052733050 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncListener.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncListener.kt @@ -1,17 +1,16 @@ package mil.nga.giat.mage.observation.sync -import android.content.Context -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource import mil.nga.giat.mage.sdk.event.IObservationEventListener class ObservationSyncListener( - val context: Context, + observationLocalDataSource: ObservationLocalDataSource, val sync : () -> Unit ): IObservationEventListener { init { - ObservationHelper.getInstance(context).addListener(this) + observationLocalDataSource.addListener(this) sync() } @@ -25,7 +24,9 @@ class ObservationSyncListener( } override fun onObservationUpdated(observation: Observation) { - if (observation.isDirty) { + if (observation.isDirty || + observation.important?.isDirty == true || + observation.favorites.any{ it.isDirty }) { sync() } } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncWorker.kt b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncWorker.kt index 84ee6bd0a..10dc2259f 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncWorker.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/sync/ObservationSyncWorker.kt @@ -11,11 +11,11 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import mil.nga.giat.mage.MageApplication import mil.nga.giat.mage.R -import mil.nga.giat.mage.data.observation.ObservationRepository -import mil.nga.giat.mage.sdk.datastore.observation.Observation -import mil.nga.giat.mage.sdk.datastore.observation.ObservationFavorite -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper -import mil.nga.giat.mage.sdk.datastore.observation.State +import mil.nga.giat.mage.data.repository.observation.ObservationRepository +import mil.nga.giat.mage.database.model.observation.Observation +import mil.nga.giat.mage.database.model.observation.ObservationFavorite +import mil.nga.giat.mage.data.datasource.observation.ObservationLocalDataSource +import mil.nga.giat.mage.database.model.observation.State import java.net.HttpURLConnection import java.util.concurrent.TimeUnit @@ -23,11 +23,10 @@ import java.util.concurrent.TimeUnit class ObservationSyncWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, - private val observationRepository: ObservationRepository + private val observationRepository: ObservationRepository, + private val observationLocalDataSource: ObservationLocalDataSource ) : CoroutineWorker(context, params) { - private val observationHelper = ObservationHelper.getInstance(applicationContext) - override suspend fun doWork(): Result { // Lock to ensure previous running work will complete when cancelled before new work is started. return mutex.withLock { @@ -61,7 +60,7 @@ class ObservationSyncWorker @AssistedInject constructor( private suspend fun syncObservations(): Int { var result = RESULT_SUCCESS_FLAG - for (observation in observationHelper.dirty) { + for (observation in observationLocalDataSource.dirty) { result = syncObservation(observation).withFlag(result) } @@ -81,7 +80,7 @@ class ObservationSyncWorker @AssistedInject constructor( private suspend fun syncObservationImportant(): Int { var result = RESULT_SUCCESS_FLAG - for (observation in observationHelper.dirtyImportant) { + for (observation in observationLocalDataSource.dirtyImportant) { result = updateImportant(observation).withFlag(result) } @@ -91,7 +90,7 @@ class ObservationSyncWorker @AssistedInject constructor( private suspend fun syncObservationFavorites(): Int { var result = RESULT_SUCCESS_FLAG - for (favorite in observationHelper.dirtyFavorites) { + for (favorite in observationLocalDataSource.dirtyFavorites) { result = updateFavorite(favorite).withFlag(result) } diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewActivity.kt b/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewActivity.kt index 6fd1e2108..c4a9c7300 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewActivity.kt @@ -24,11 +24,12 @@ import mil.nga.giat.mage.form.edit.dialog.FormReorderDialog import mil.nga.giat.mage.observation.attachment.AttachmentViewActivity import mil.nga.giat.mage.observation.edit.ObservationEditActivity import mil.nga.giat.mage.people.PeopleActivity -import mil.nga.giat.mage.sdk.datastore.observation.Attachment -import mil.nga.giat.mage.sdk.datastore.user.Permission -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.observation.Attachment +import mil.nga.giat.mage.database.model.permission.Permission +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource import mil.nga.giat.mage.utils.googleMapsUri +import javax.inject.Inject @AndroidEntryPoint class ObservationViewActivity : AppCompatActivity() { @@ -52,6 +53,8 @@ class ObservationViewActivity : AppCompatActivity() { private lateinit var viewModel: FormViewModel + @Inject lateinit var userLocalDataSource: UserLocalDataSource + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,8 +68,9 @@ class ObservationViewActivity : AppCompatActivity() { viewModel.setObservation(observationId, observeChanges = true, defaultMapZoom, defaultMapCenter) try { - currentUser = UserHelper.getInstance(this).readCurrentUser() - hasEventUpdatePermission = currentUser?.role?.permissions?.permissions?.contains(Permission.UPDATE_EVENT) ?: false + currentUser = userLocalDataSource.readCurrentUser() + hasEventUpdatePermission = currentUser?.role?.permissions?.permissions?.contains( + Permission.UPDATE_EVENT) ?: false } catch (e: Exception) { Log.e(LOG_NAME, "Cannot read current user") } @@ -95,7 +99,7 @@ class ObservationViewActivity : AppCompatActivity() { } private fun onEditObservation() { - if (!UserHelper.getInstance(applicationContext).isCurrentUserPartOfCurrentEvent) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { AlertDialog.Builder(this) .setTitle(R.string.no_event_title) .setMessage(R.string.observation_no_event_edit_message) diff --git a/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewScreen.kt b/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewScreen.kt index be0bec806..52f8d3e8e 100644 --- a/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewScreen.kt +++ b/mage/src/main/java/mil/nga/giat/mage/observation/view/ObservationViewScreen.kt @@ -25,13 +25,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import mil.nga.giat.mage.compat.server5.form.view.AttachmentsViewContentServer5 import mil.nga.giat.mage.coordinate.CoordinateFormatter +import mil.nga.giat.mage.database.model.event.Event import mil.nga.giat.mage.form.FormViewModel import mil.nga.giat.mage.form.view.* import mil.nga.giat.mage.observation.ObservationPermission import mil.nga.giat.mage.observation.ObservationState import mil.nga.giat.mage.observation.ObservationStatusState import mil.nga.giat.mage.sdk.Compatibility -import mil.nga.giat.mage.sdk.datastore.observation.Attachment +import mil.nga.giat.mage.database.model.observation.Attachment import mil.nga.giat.mage.ui.theme.MageTheme import mil.nga.giat.mage.ui.theme.importantBackground import mil.nga.giat.mage.ui.theme.linkColor @@ -71,7 +72,8 @@ fun ObservationViewScreen( content = { Column { ObservationViewContent( - observationState, + event = viewModel.event, + observationState = observationState, onAction = onAction, onLocationClick = onLocationClick, onAttachmentClick = onAttachmentClick @@ -113,6 +115,7 @@ fun ObservationViewTopBar( @Composable fun ObservationViewContent( + event: Event?, observationState: ObservationState?, onAction: ((ObservationAction) -> Unit)? = null, onLocationClick: ((String) -> Unit)? = null, @@ -143,6 +146,7 @@ fun ObservationViewContent( val forms by observationState.forms ObservationViewHeaderContent( + event = event, observationState = observationState, onAction = onAction, onLocationClick = { onLocationClick?.invoke(it) } @@ -347,6 +351,7 @@ fun ObservationErrorStatus( @Composable fun ObservationViewHeaderContent( + event: Event?, observationState: ObservationState? = null, onLocationClick: ((String) -> Unit)? = null, onAction: ((ObservationAction) -> Unit)? = null @@ -440,7 +445,13 @@ fun ObservationViewHeaderContent( ) { val mapView = rememberMapViewWithLifecycle() val mapState = MapState(observationState.geometryFieldState.defaultMapCenter, observationState.geometryFieldState.defaultMapZoom) - MapViewContent(mapView, mapState, formState, location) + MapViewContent( + map = mapView, + event = event, + mapState = mapState, + formState = formState, + location = location + ) } val locationText = CoordinateFormatter(LocalContext.current).format(location.centroidLatLng) diff --git a/mage/src/main/java/mil/nga/giat/mage/people/PeopleActivity.java b/mage/src/main/java/mil/nga/giat/mage/people/PeopleActivity.java index 244d89d41..169ebc021 100644 --- a/mage/src/main/java/mil/nga/giat/mage/people/PeopleActivity.java +++ b/mage/src/main/java/mil/nga/giat/mage/people/PeopleActivity.java @@ -13,12 +13,20 @@ import java.util.Collection; import java.util.List; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import mil.nga.giat.mage.R; +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource; +import mil.nga.giat.mage.database.model.event.Event; +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource; +import mil.nga.giat.mage.database.model.team.Team; import mil.nga.giat.mage.profile.ProfileActivity; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; +import mil.nga.giat.mage.database.model.user.User; +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource; import mil.nga.giat.mage.sdk.exceptions.UserException; +@AndroidEntryPoint public class PeopleActivity extends AppCompatActivity implements PeopleRecyclerAdapter.OnPersonClickListener { public static final String USER_REMOTE_IDS = "USER_REMOTE_IDS"; @@ -29,6 +37,10 @@ public class PeopleActivity extends AppCompatActivity implements PeopleRecyclerA private RecyclerView recyclerView; private PeopleRecyclerAdapter adapter; + @Inject protected UserLocalDataSource userLocalDataSource; + @Inject protected TeamLocalDataSource teamLocalDataSource; + @Inject protected EventLocalDataSource eventLocalDataSource; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -40,12 +52,20 @@ public void onCreate(Bundle savedInstanceState) { Collection userIds = getIntent().getStringArrayListExtra(USER_REMOTE_IDS); try { - people = UserHelper.getInstance(getApplicationContext()).read(userIds); + people = userLocalDataSource.read(userIds); } catch (UserException e) { - Log.e(LOG_NAME, "Error read users for remoteIds: " + userIds.toString(), e); + Log.e(LOG_NAME, "Error read users for remoteIds: " + userIds, e); } - adapter = new PeopleRecyclerAdapter(this, people); + Event event = eventLocalDataSource.getCurrentEvent(); + List eventTeams = teamLocalDataSource.getTeamsByEvent(event); + + adapter = new PeopleRecyclerAdapter( + getApplicationContext(), + people, + eventTeams, + teamLocalDataSource + ); recyclerView.setAdapter(adapter); adapter.setOnPersonClickListener(this); } diff --git a/mage/src/main/java/mil/nga/giat/mage/people/PeopleRecyclerAdapter.java b/mage/src/main/java/mil/nga/giat/mage/people/PeopleRecyclerAdapter.java index c814d4ec9..02360d065 100644 --- a/mage/src/main/java/mil/nga/giat/mage/people/PeopleRecyclerAdapter.java +++ b/mage/src/main/java/mil/nga/giat/mage/people/PeopleRecyclerAdapter.java @@ -8,6 +8,7 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.graphics.drawable.RoundedBitmapDrawable; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.recyclerview.widget.RecyclerView; @@ -21,47 +22,45 @@ import java.util.List; import mil.nga.giat.mage.R; +import mil.nga.giat.mage.data.datasource.team.TeamLocalDataSource; import mil.nga.giat.mage.glide.GlideApp; import mil.nga.giat.mage.glide.model.Avatar; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.datastore.user.Team; -import mil.nga.giat.mage.sdk.datastore.user.TeamHelper; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserLocal; - -/** - * Created by wnewman on 8/26/16. - */ +import mil.nga.giat.mage.database.model.team.Team; +import mil.nga.giat.mage.database.model.user.User; +import mil.nga.giat.mage.database.model.user.UserLocal; + public class PeopleRecyclerAdapter extends RecyclerView.Adapter { public interface OnPersonClickListener { void onPersonClick(User person); } - private Event event; - private List people; - private Context context; - private TeamHelper teamHelper; - private Collection eventTeams; + private final List people; + private final Context context; + private final Collection eventTeams; + private final TeamLocalDataSource teamLocalDataSource; private OnPersonClickListener personClickListener; - public PeopleRecyclerAdapter(Context context, List people) { + public PeopleRecyclerAdapter( + Context context, + List people, + Collection eventTeams, + TeamLocalDataSource teamLocalDataSource + ) { this.context = context; this.people = people; - this.teamHelper = TeamHelper.getInstance(context); - this.event = EventHelper.getInstance(context).getCurrentEvent(); - eventTeams = teamHelper.getTeamsByEvent(event); + this.eventTeams = eventTeams; + this.teamLocalDataSource = teamLocalDataSource; } public void setOnPersonClickListener(OnPersonClickListener personClickListener) { this.personClickListener = personClickListener; } + @NonNull @Override public PersonViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) { View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.favorite_user_list_item, viewGroup, false); - PersonViewHolder viewHolder = new PersonViewHolder(view); - return viewHolder; + return new PersonViewHolder(view); } @Override @@ -96,9 +95,9 @@ protected void setResource(Bitmap resource) { viewHolder.name.setText(user.getDisplayName()); - Collection userTeams = teamHelper.getTeamsByUser(user); + Collection userTeams = teamLocalDataSource.getTeamsByUser(user); userTeams.retainAll(eventTeams); - Collection teamNames = Collections2.transform(userTeams, team -> team.getName()); + Collection teamNames = Collections2.transform(userTeams, Team::getName); viewHolder.teams.setText(StringUtils.join(teamNames, ", ")); } @@ -108,7 +107,7 @@ public int getItemCount() { return people.size(); } - public class PersonViewHolder extends RecyclerView.ViewHolder { + public static class PersonViewHolder extends RecyclerView.ViewHolder { protected View card; protected TextView name; protected TextView teams; diff --git a/mage/src/main/java/mil/nga/giat/mage/preferences/LocationPreferencesActivity.java b/mage/src/main/java/mil/nga/giat/mage/preferences/LocationPreferencesActivity.java index 348789d45..cab92b3b3 100644 --- a/mage/src/main/java/mil/nga/giat/mage/preferences/LocationPreferencesActivity.java +++ b/mage/src/main/java/mil/nga/giat/mage/preferences/LocationPreferencesActivity.java @@ -1,11 +1,8 @@ package mil.nga.giat.mage.preferences; -import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.Settings; @@ -14,12 +11,8 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.CompoundButton; import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.SwitchCompat; -import androidx.appcompat.widget.Toolbar; -import androidx.core.content.ContextCompat; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; @@ -30,7 +23,7 @@ import mil.nga.giat.mage.MageApplication; import mil.nga.giat.mage.R; import mil.nga.giat.mage.location.LocationAccess; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource; @AndroidEntryPoint public class LocationPreferencesActivity extends AppCompatActivity { @@ -41,10 +34,10 @@ public class LocationPreferencesActivity extends AppCompatActivity { @Inject protected @ApplicationContext Context context; @Inject protected LocationAccess locationAccess; - @AndroidEntryPoint public static class LocationPreferenceFragment extends PreferenceFragmentCompat { @Inject protected @ApplicationContext Context context; + @Inject protected UserLocalDataSource userLocalDataSource; @Inject protected LocationAccess locationAccess; @Override @@ -57,7 +50,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa final Context contextThemeWrapper = new ContextThemeWrapper(getActivity(), R.style.AppTheme); LayoutInflater localInflater = inflater.cloneInContext(contextThemeWrapper); - if (!UserHelper.getInstance(context).isCurrentUserPartOfCurrentEvent()) { + if (!userLocalDataSource.isCurrentUserPartOfCurrentEvent()) { Preference reportLocationPreference = findPreference(getString(R.string.reportLocationKey)); reportLocationPreference.setEnabled(false); reportLocationPreference.setSummary(R.string.location_no_event_message); diff --git a/mage/src/main/java/mil/nga/giat/mage/profile/AvatarSyncWorker.kt b/mage/src/main/java/mil/nga/giat/mage/profile/AvatarSyncWorker.kt new file mode 100644 index 000000000..0529a6a1f --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/profile/AvatarSyncWorker.kt @@ -0,0 +1,55 @@ +package mil.nga.giat.mage.profile + +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import java.util.concurrent.TimeUnit + +@HiltWorker +class AvatarSyncWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val userRepository: UserRepository, + private val userLocalDataSource: UserLocalDataSource +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + return try { + userLocalDataSource.readCurrentUser()?.let { user-> + userRepository.syncAvatar(user.avatarPath) + } + Result.success() + } catch (e: Exception) { + Log.e(LOG_NAME, "Failed to sync user avatar", e) + Result.retry() + } + } + + companion object { + private val LOG_NAME = AvatarSyncWorker::class.java.simpleName + + private const val AVATAR_SYNC_WORK = "mil.nga.mage.AVATAR_SYNC_WORK" + + fun scheduleWork(context: Context) { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + val request = OneTimeWorkRequest.Builder(AvatarSyncWorker::class.java) + .setConstraints(constraints) + .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + .setBackoffCriteria(BackoffPolicy.LINEAR, 15, TimeUnit.SECONDS) + .build() + + WorkManager + .getInstance(context) + .beginUniqueWork(AVATAR_SYNC_WORK, ExistingWorkPolicy.KEEP, request) + .enqueue() + } + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.java b/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.java deleted file mode 100644 index f5bf0e594..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.java +++ /dev/null @@ -1,226 +0,0 @@ -package mil.nga.giat.mage.profile; - -import android.content.DialogInterface; -import android.content.Intent; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.Log; -import android.view.MenuItem; -import android.view.View; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.material.textfield.TextInputEditText; -import com.google.android.material.textfield.TextInputLayout; -import com.google.gson.JsonObject; -import com.nulabinc.zxcvbn.Zxcvbn; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import javax.inject.Inject; - -import dagger.hilt.android.AndroidEntryPoint; -import mil.nga.giat.mage.MageApplication; -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.login.LoginActivity; -import mil.nga.giat.mage.login.PasswordStrengthFragment; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.exceptions.UserException; -import mil.nga.giat.mage.sdk.http.resource.UserResource; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -/** - * Created by wnewman on 12/14/17. - */ - -@AndroidEntryPoint -public class ChangePasswordActivity extends AppCompatActivity { - - private static final String LOG_NAME = ChangePasswordActivity.class.getName(); - - @Inject - MageApplication application; - - private String username; - - private TextInputEditText password; - private TextInputLayout passwordLayout; - - private TextInputEditText newPassword; - private TextInputLayout newPasswordLayout; - - private TextInputEditText newPasswordConfirm; - private TextInputLayout newPasswordConfirmLayout; - - private final Zxcvbn zxcvbn = new Zxcvbn(); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.activity_change_password); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); - - final PasswordStrengthFragment passwordStrengthFragment = (PasswordStrengthFragment) getSupportFragmentManager().findFragmentById(R.id.password_strength_fragment); - - try { - User user = UserHelper.getInstance(getApplicationContext()).readCurrentUser(); - username = user.getUsername(); - - List sanitizedPasswordInputs = new ArrayList<>(); - sanitizedPasswordInputs.add(user.getUsername()); - sanitizedPasswordInputs.add(user.getDisplayName()); - sanitizedPasswordInputs.add(user.getEmail()); - sanitizedPasswordInputs.removeAll(Collections.singleton(null)); - passwordStrengthFragment.setSanitizedList(sanitizedPasswordInputs); - } catch (UserException ue) { - Log.e(LOG_NAME, "Problem finding current user.", ue); - } - - password = (TextInputEditText) findViewById(R.id.password); - passwordLayout = (TextInputLayout) findViewById(R.id.password_layout); - - newPassword = (TextInputEditText) findViewById(R.id.new_password); - newPasswordLayout = (TextInputLayout) findViewById(R.id.new_password_layout); - - newPasswordConfirm = (TextInputEditText) findViewById(R.id.new_password_confirm); - newPasswordConfirmLayout = (TextInputLayout) findViewById(R.id.new_password_confirm_layout); - - newPassword = (TextInputEditText) findViewById(R.id.new_password); - newPassword.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - - } - - @Override - public void afterTextChanged(Editable s) { - passwordStrengthFragment.onPasswordChanged(s.toString()); - } - }); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - this.finish(); - return true; - } - - return true; - } - - public void onChangePasswordClick(View v) { - if (!validateInputs()) { - return; - } - - UserResource userResource = new UserResource(getApplicationContext()); - userResource.changePassword(username, password.getText().toString(), newPassword.getText().toString(), newPasswordConfirm.getText().toString(), new Callback() { - @Override - public void onResponse(Call call, Response response) { - if (response.isSuccessful()) { - onSuccess(); - } else { - onError(response); - } - } - - @Override - public void onFailure(Call call, Throwable t) { - onError(null); - } - }); - } - - private void onSuccess() { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle("Password Changed") - .setMessage("Your passsword has been changed, for security purposes you will need to login with your new password.") - .setCancelable(false) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - application.onLogout(true, null); - Intent intent = new Intent(ChangePasswordActivity.this, LoginActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); - finish(); - } - }).create(); - - dialog.show(); - } - - private void onError(Response response) { - if (response == null) { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle("No connection") - .setMessage("Please ensure you have an internet connection and try again") - .setCancelable(false) - .setPositiveButton(android.R.string.ok, null).create(); - - dialog.show(); - } else { - int errorCode = response.code(); - if (errorCode == 401) { - passwordLayout.setError("Invalid password, please check your password and try again"); - } else { - AlertDialog dialog = new AlertDialog.Builder(this) - .setTitle("Error changing password") - .setMessage(response.message()) - .setCancelable(false) - .setPositiveButton(android.R.string.ok, null).create(); - - dialog.show(); - } - } - } - - private boolean validateInputs() { - passwordLayout.setError(null); - newPasswordLayout.setError(null); - newPasswordConfirmLayout.setError(null); - - if (StringUtils.isBlank(password.getText().toString())) { - passwordLayout.setError("Password is required"); - return false; - } - - if (StringUtils.isBlank(newPassword.getText().toString())) { - newPasswordLayout.setError("New Password is required"); - return false; - } - - if (!newPassword.getText().toString().equals(newPasswordConfirm.getText().toString())) { - newPasswordLayout.setError("Passwords do not match"); - newPasswordConfirmLayout.setError("Passwords do not match"); - return false; - } - - if (password.getText().toString().equals(newPassword.getText().toString())) { - newPasswordLayout.setError("New password cannot be the same as current password."); - newPasswordConfirmLayout.setError("New password cannot be the same as current password."); - return false; - } - - return true; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.kt b/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.kt new file mode 100644 index 000000000..9ba22a7d0 --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/profile/ChangePasswordActivity.kt @@ -0,0 +1,188 @@ +package mil.nga.giat.mage.profile + +import android.content.Intent +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import com.google.gson.JsonObject +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mil.nga.giat.mage.MageApplication +import mil.nga.giat.mage.R +import mil.nga.giat.mage.data.repository.user.UserRepository +import mil.nga.giat.mage.login.LoginActivity +import mil.nga.giat.mage.login.PasswordStrengthFragment +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import mil.nga.giat.mage.sdk.exceptions.UserException +import org.apache.commons.lang3.StringUtils +import retrofit2.Response +import java.lang.Exception +import javax.inject.Inject + +@AndroidEntryPoint +class ChangePasswordActivity : AppCompatActivity() { + @Inject lateinit var application: MageApplication + @Inject lateinit var userRepository: UserRepository + @Inject lateinit var userLocalDataSource: UserLocalDataSource + + private var username: String? = null + private lateinit var password: TextInputEditText + private lateinit var passwordLayout: TextInputLayout + private lateinit var newPassword: TextInputEditText + private lateinit var newPasswordLayout: TextInputLayout + private lateinit var newPasswordConfirm: TextInputEditText + private lateinit var newPasswordConfirmLayout: TextInputLayout + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_change_password) + + supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + it.setHomeAsUpIndicator(R.drawable.ic_close_white_24dp) + } + + val passwordStrengthFragment = + supportFragmentManager.findFragmentById(R.id.password_strength_fragment) as PasswordStrengthFragment? + + try { + userLocalDataSource.readCurrentUser()?.let { user -> + username = user.username + val sanitizedPasswordInputs: MutableList = ArrayList() + sanitizedPasswordInputs.add(user.username) + sanitizedPasswordInputs.add(user.displayName) + sanitizedPasswordInputs.add(user.email) + sanitizedPasswordInputs.removeAll(setOf(null)) + passwordStrengthFragment!!.setSanitizedList(sanitizedPasswordInputs) + } + } catch (e: UserException) { + Log.e(LOG_NAME, "Problem finding current user.", e) + } + + password = findViewById(R.id.password) as TextInputEditText + passwordLayout = findViewById(R.id.password_layout) as TextInputLayout + newPassword = findViewById(R.id.new_password) as TextInputEditText + newPasswordLayout = findViewById(R.id.new_password_layout) as TextInputLayout + newPasswordConfirm = findViewById(R.id.new_password_confirm) as TextInputEditText + newPasswordConfirmLayout = findViewById(R.id.new_password_confirm_layout) as TextInputLayout + newPassword = findViewById(R.id.new_password) as TextInputEditText + newPassword.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + passwordStrengthFragment!!.onPasswordChanged(s.toString()) + } + }) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + finish() + return true + } + } + return true + } + + fun onChangePasswordClick(v: View?) { + if (!validateInputs()) { + return + } + + CoroutineScope(Dispatchers.IO).launch { + try { + val response = userRepository.changePassword( + username, + password.text.toString(), + newPassword.text.toString(), + newPasswordConfirm.text.toString()) + + if (response.isSuccessful) { + onSuccess() + } else { + onError(response) + } + } catch (e: Exception) { + onError(null) + } + + } + } + + private fun onSuccess() { + val dialog = AlertDialog.Builder(this) + .setTitle("Password Changed") + .setMessage("Your password has been changed, for security purposes you will need to login with your new password.") + .setCancelable(false) + .setPositiveButton(android.R.string.ok) { dialog, which -> + application.onLogout(true) + val intent = Intent(this@ChangePasswordActivity, LoginActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + }.create() + dialog.show() + } + + private fun onError(response: Response?) { + if (response == null) { + val dialog = AlertDialog.Builder(this) + .setTitle("No connection") + .setMessage("Please ensure you have an internet connection and try again") + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null).create() + dialog.show() + } else { + val errorCode = response.code() + if (errorCode == 401) { + passwordLayout.error = "Invalid password, please check your password and try again" + } else { + val dialog = AlertDialog.Builder(this) + .setTitle("Error changing password") + .setMessage(response.message()) + .setCancelable(false) + .setPositiveButton(android.R.string.ok, null).create() + dialog.show() + } + } + } + + private fun validateInputs(): Boolean { + passwordLayout.error = null + newPasswordLayout.error = null + newPasswordConfirmLayout.error = null + if (StringUtils.isBlank(password.text.toString())) { + passwordLayout.error = "Password is required" + return false + } + if (StringUtils.isBlank(newPassword.text.toString())) { + newPasswordLayout.error = "New Password is required" + return false + } + if (newPassword.text.toString() != newPasswordConfirm.text.toString()) { + newPasswordLayout.error = "Passwords do not match" + newPasswordConfirmLayout.error = "Passwords do not match" + return false + } + if (password.text.toString() == newPassword.text.toString()) { + newPasswordLayout.error = "New password cannot be the same as current password." + newPasswordConfirmLayout.error = "New password cannot be the same as current password." + return false + } + return true + } + + companion object { + private val LOG_NAME = ChangePasswordActivity::class.java.name + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/profile/ProfileActivity.java b/mage/src/main/java/mil/nga/giat/mage/profile/ProfileActivity.java index 4284694fc..f400000a6 100644 --- a/mage/src/main/java/mil/nga/giat/mage/profile/ProfileActivity.java +++ b/mage/src/main/java/mil/nga/giat/mage/profile/ProfileActivity.java @@ -2,19 +2,16 @@ import android.Manifest; import android.app.Activity; -import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; -import android.os.Build; import android.os.Bundle; +import android.os.Environment; import android.preference.PreferenceManager; import android.provider.MediaStore; import android.provider.Settings; @@ -28,10 +25,14 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContract; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import androidx.core.graphics.ColorUtils; @@ -51,15 +52,12 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.UUID; import javax.inject.Inject; import dagger.hilt.android.AndroidEntryPoint; -import mil.nga.giat.mage.BuildConfig; import mil.nga.giat.mage.MageApplication; import mil.nga.giat.mage.R; import mil.nga.giat.mage.glide.GlideApp; @@ -69,15 +67,14 @@ import mil.nga.giat.mage.login.LoginActivity; import mil.nga.giat.mage.map.MapAndViewProvider; import mil.nga.giat.mage.map.annotation.MapAnnotation; -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper; -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; +import mil.nga.giat.mage.database.model.location.Location; +import mil.nga.giat.mage.data.datasource.location.LocationLocalDataSource; +import mil.nga.giat.mage.database.model.location.LocationProperty; +import mil.nga.giat.mage.database.model.event.Event; +import mil.nga.giat.mage.data.datasource.event.EventLocalDataSource; +import mil.nga.giat.mage.database.model.user.User; +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource; import mil.nga.giat.mage.sdk.exceptions.UserException; -import mil.nga.giat.mage.sdk.profile.UpdateProfileTask; import mil.nga.giat.mage.sdk.utils.MediaUtility; import mil.nga.giat.mage.utils.GeometryKt; import mil.nga.giat.mage.widget.CoordinateView; @@ -93,19 +90,13 @@ public enum ResultType { NAVIGATE } private static final String CURRENT_MEDIA_PATH = "CURRENT_MEDIA_PATH"; - private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100; - private static final int GALLERY_ACTIVITY_REQUEST_CODE = 200; - private static final int PERMISSIONS_REQUEST_CAMERA = 300; - private static final int PERMISSIONS_REQUEST_STORAGE = 400; - public static String USER_ID_EXTRA = "USER_ID_EXTRA"; public static String RESULT_TYPE_EXTRA = "RESULT_TYPE_EXTRA"; - @Inject - MageApplication application; - - @Inject - SharedPreferences preferences; + @Inject MageApplication application; + @Inject UserLocalDataSource userLocalDataSource; + @Inject EventLocalDataSource eventLocalDataSource; + @Inject LocationLocalDataSource locationLocalDataSource; private String currentMediaPath; private User user; @@ -120,7 +111,30 @@ public enum ResultType { NAVIGATE } private SupportMapFragment mapFragment; private TextView phone; - private TextView email; + + public static class TakeSelfie extends ActivityResultContract { + @NonNull + @Override + public Intent createIntent(@NonNull Context context, @NonNull Uri input) { + return new Intent(MediaStore.ACTION_IMAGE_CAPTURE) + .putExtra("android.intent.extra.USE_FRONT_CAMERA", true) + .putExtra(MediaStore.EXTRA_OUTPUT, input); + } + + @Override + public Boolean parseResult(int resultCode, @Nullable Intent result) { + return resultCode == Activity.RESULT_OK; + } + } + + private final ActivityResultLauncher requestPermissions = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), this::onCameraPermission); + + private final ActivityResultLauncher getCameraAvatar = + registerForActivityResult(new TakeSelfie(), this::onImageResult); + + private final ActivityResultLauncher getGalleryAvatar = + registerForActivityResult(new ActivityResultContracts.GetContent(), this::onDocumentResult); @Override public void onCreate(Bundle savedInstanceState) { @@ -134,15 +148,15 @@ public void onCreate(Bundle savedInstanceState) { long userId = getIntent().getLongExtra(USER_ID_EXTRA, -1); try { if (userId != -1) { - user = UserHelper.getInstance(context).read(userId); + user = userLocalDataSource.read(userId); isCurrentUser = false; } else { - user = UserHelper.getInstance(context).readCurrentUser(); + user = userLocalDataSource.readCurrentUser(); isCurrentUser = true; } - Event event = EventHelper.getInstance(context).getCurrentEvent(); - List locations = LocationHelper.getInstance(context).getUserLocations(user.getId(), event.getId(), 1, true); + Event event = eventLocalDataSource.getCurrentEvent(); + List locations = locationLocalDataSource.getUserLocations(user.getId(), event.getId(), 1, true); if (!locations.isEmpty()) { location = locations.get(0); Point point = GeometryUtils.getCentroid(location.getGeometry()); @@ -177,7 +191,7 @@ public void onCreate(Bundle savedInstanceState) { phoneLayout.setVisibility(View.GONE); } - email = findViewById(R.id.email); + TextView email = findViewById(R.id.email); View emailLayout = findViewById(R.id.email_layout); emailLayout.setOnLongClickListener(v -> { onEmailLongCLick(v); @@ -238,7 +252,7 @@ public void onCreate(Bundle savedInstanceState) { avatarBottomSheetView.findViewById(R.id.gallery_avatar_layout).setOnClickListener(v -> updateAvatarFromGallery()); - avatarBottomSheetView.findViewById(R.id.camera_avatar_layout).setOnClickListener(v -> updateAvatarFromCamera()); + avatarBottomSheetView.findViewById(R.id.camera_avatar_layout).setOnClickListener(v -> onCameraAction()); if (isCurrentUser) { profileActionDialog = new BottomSheetDialog(ProfileActivity.this); @@ -252,7 +266,7 @@ public void onCreate(Bundle savedInstanceState) { } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(@NonNull Menu menu) { if (isCurrentUser) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.profile_menu, menu); @@ -287,28 +301,32 @@ private void viewAvatar() { avatarActionsDialog.cancel(); } - private void updateAvatarFromGallery() { - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(ProfileActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_STORAGE); - } else { - launchGalleryIntent(); - } - avatarActionsDialog.cancel(); + private void onCameraAction() { + requestPermissions.launch(Manifest.permission.CAMERA); } private void updateAvatarFromCamera() { - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || - ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(ProfileActivity.this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSIONS_REQUEST_CAMERA); - } else { - launchCameraIntent(); - } + File file = new File( + getExternalFilesDir(Environment.DIRECTORY_PICTURES), + UUID.randomUUID().toString() + ".jpg" + ); + currentMediaPath = file.getAbsolutePath(); + + Uri uri = FileProvider.getUriForFile(getApplicationContext(), getApplicationContext().getPackageName() + ".fileprovider", file); + + getCameraAvatar.launch(uri); + + avatarActionsDialog.cancel(); + } + + private void updateAvatarFromGallery() { + getGalleryAvatar.launch("image/*"); avatarActionsDialog.cancel(); } private void logout() { - application.onLogout(true, null); - Intent intent = new Intent(ProfileActivity.this, LoginActivity.class); + application.onLogout(true); + Intent intent = new Intent(getApplicationContext(), LoginActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); finish(); @@ -327,103 +345,6 @@ private void onAvatarClick() { } } - private void launchCameraIntent() { - try { - File file = MediaUtility.createImageFile(); - currentMediaPath = file.getAbsolutePath(); - Uri uri = getUriForFile(file); - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); - intent.putExtra("android.intent.extra.USE_FRONT_CAMERA", true); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); - } catch (IOException e) { - Log.e(LOG_NAME, "Error creating video media file", e); - } - } - - private Uri getUriForFile(File file) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", file); - } else { - return Uri.fromFile(file); - } - } - - private void launchGalleryIntent() { - Intent intent = new Intent(); - intent.setType("image/*"); - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); - startActivityForResult(intent, GALLERY_ACTIVITY_REQUEST_CODE); - } - - @Override - public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - switch (requestCode) { - case PERMISSIONS_REQUEST_CAMERA: { - Map grants = new HashMap<>(); - grants.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_GRANTED); - grants.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED); - - for (int i = 0; i < grantResults.length; i++) { - grants.put(permissions[i], grantResults[i]); - } - - if (grants.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && - grants.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { - launchCameraIntent(); - } else if ((!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) && grants.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) || - (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) && grants.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) || - !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - - // User denied camera or storage with never ask again. Since they will get here - // by clicking the camera button give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - getResources().getString(R.string.camera_access_title), - getResources().getString(R.string.camera_access_message)); - } - - break; - } - case PERMISSIONS_REQUEST_STORAGE: { - if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - launchGalleryIntent(); - } else { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { - // User denied storage with never ask again. Since they will get here - // by clicking the gallery button give them a dialog that will - // guide them to settings if they want to enable the permission - showDisabledPermissionsDialog( - getResources().getString(R.string.gallery_access_title), - getResources().getString(R.string.gallery_access_message)); - } - } - - break; - } - } - } - - private void showDisabledPermissionsDialog(String title, String message) { - new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle) - .setTitle(title) - .setMessage(message) - .setPositiveButton(R.string.settings, new Dialog.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setData(Uri.fromParts("package", getApplicationContext().getPackageName(), null)); - startActivity(intent); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - @Override public void onMapAndViewReady(GoogleMap map) { SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); @@ -447,7 +368,7 @@ public void onMapAndViewReady(GoogleMap map) { LocationAgeTransformation transformation = new LocationAgeTransformation(application, location.getTimestamp().getTime()); - MapAnnotation feature = MapAnnotation.Companion.fromUser(user, location); + MapAnnotation feature = MapAnnotation.Companion.fromUser(user, location); Glide.with(this) .asBitmap() .load(feature) @@ -480,85 +401,100 @@ public void onMapAndViewReady(GoogleMap map) { } } } - + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(CURRENT_MEDIA_PATH, currentMediaPath); + } + @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); + protected void onRestoreInstanceState(Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); - if (resultCode != AppCompatActivity.RESULT_OK) { - return; + currentMediaPath = savedInstanceState.getString(CURRENT_MEDIA_PATH); + } + + private void onCameraPermission(Boolean result) { + if (!result && !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { + new AlertDialog.Builder(this, R.style.AppCompatAlertDialogStyle) + .setTitle(getResources().getString(R.string.camera_access_title)) + .setMessage(getResources().getString(R.string.camera_access_message)) + .setPositiveButton(R.string.settings, (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getApplicationContext().getPackageName(), null)); + startActivity(intent); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else if (result) { + updateAvatarFromCamera(); } - String filePath = null; - switch (requestCode) { - case CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE: - filePath = currentMediaPath; - File file = new File(currentMediaPath); - MediaUtility.addImageToGallery(getApplicationContext(), Uri.fromFile(file)); - break; - case GALLERY_ACTIVITY_REQUEST_CODE: - List uris = getUris(data); - for (Uri uri : uris) { - try { - File avatarFile = MediaUtility.copyMediaFromUri(this, uri); - filePath = avatarFile.getAbsolutePath(); - } catch (IOException e) { - Log.e(LOG_NAME, "Error copying gallery file for avatar to local storage", e); - } + } + + private void onImageResult(Boolean result) { + if (result) { + if (currentMediaPath != null) { + final Context context = getApplicationContext(); + try { + user = userLocalDataSource.setAvatarPath(user, currentMediaPath); + } catch (UserException e) { + Log.e(LOG_NAME, "Error setting local avatar path", e); } - break; + + final ImageView iv = findViewById(R.id.avatar); + GlideApp.with(context) + .load(Avatar.Companion.forUser(user)) + .circleCrop() + .into(iv); + + AvatarSyncWorker.Companion.scheduleWork(getApplicationContext()); + } } - if (filePath != null) { + currentMediaPath = null; + } + + private void onDocumentResult(Uri uri) { + try { + File avatarFile = MediaUtility.copyMediaFromUri(this, uri); + String filePath = avatarFile.getAbsolutePath(); + final Context context = getApplicationContext(); try { - user = UserHelper.getInstance(context).setAvatarPath(user, filePath); + user = userLocalDataSource.setAvatarPath(user, filePath); } catch (UserException e) { Log.e(LOG_NAME, "Error setting local avatar path", e); } final ImageView iv = findViewById(R.id.avatar); GlideApp.with(context) - .load(Avatar.Companion.forUser(user)) - .circleCrop() - .into(iv); + .load(Avatar.Companion.forUser(user)) + .circleCrop() + .into(iv); - UpdateProfileTask task = new UpdateProfileTask(user, this); - task.execute(filePath); + AvatarSyncWorker.Companion.scheduleWork(getApplicationContext()); + } catch (IOException e) { + Log.e(LOG_NAME, "Error copying gallery file for avatar to local storage", e); } } - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putString(CURRENT_MEDIA_PATH, currentMediaPath); - } - - @Override - protected void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - - currentMediaPath = savedInstanceState.getString(CURRENT_MEDIA_PATH); - } - public void onLocationClick(View view) { new AlertDialog.Builder(this) .setTitle(getResources().getString(R.string.navigation_choice_title)) - .setItems(R.array.navigationOptions, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - switch (which) { - case 0: { - Intent intent = new Intent(Intent.ACTION_VIEW, GeometryKt.googleMapsUri(location.getGeometry())); - startActivity(intent); - break; - } - case 1: { - Intent intent = new Intent(); - intent.putExtra(USER_ID_EXTRA, user.getId()); - intent.putExtra(RESULT_TYPE_EXTRA, ResultType.NAVIGATE); - setResult(Activity.RESULT_OK, intent); - finish(); - } + .setItems(R.array.navigationOptions, (dialog, which) -> { + switch (which) { + case 0: { + Intent intent = new Intent(Intent.ACTION_VIEW, GeometryKt.googleMapsUri(location.getGeometry())); + startActivity(intent); + break; + } + case 1: { + Intent intent = new Intent(); + intent.putExtra(USER_ID_EXTRA, user.getId()); + intent.putExtra(RESULT_TYPE_EXTRA, ResultType.NAVIGATE); + setResult(Activity.RESULT_OK, intent); + finish(); } } }) @@ -604,27 +540,24 @@ public void onPhoneClick(View view) { private void onPhoneLongCLick(final View view) { String[] items = {"Call", "Send a message", "Copy to clipboard", }; View titleView = getLayoutInflater().inflate(R.layout.alert_primary_title, null); - TextView title = (TextView) titleView.findViewById(R.id.alertTitle); + TextView title = titleView.findViewById(R.id.alertTitle); title.setText(user.getPrimaryPhone()); - ImageView icon = (ImageView) titleView.findViewById(R.id.icon); + ImageView icon = titleView.findViewById(R.id.icon); icon.setImageResource(R.drawable.ic_phone_white_24dp); AlertDialog dialog = new AlertDialog.Builder(this) .setCustomTitle(titleView) - .setItems(items, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int item) { - if (item == 0) { - onPhoneClick(view); - } else if (item == 1) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("sms:" + user.getPrimaryPhone())); - startActivity(intent); - } else { - ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("Phone", user.getPrimaryPhone()); - clipboard.setPrimaryClip(clip); - } + .setItems(items, (dialog1, item) -> { + if (item == 0) { + onPhoneClick(view); + } else if (item == 1) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("sms:" + user.getPrimaryPhone())); + startActivity(intent); + } else { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("Phone", user.getPrimaryPhone()); + clipboard.setPrimaryClip(clip); } }) .create(); @@ -660,25 +593,4 @@ private void onEmailLongCLick(final View view) { .create() .show(); } - - private List getUris(Intent intent) { - List uris = new ArrayList(); - uris.addAll(getClipDataUris(intent)); - if (intent.getData() != null) { - uris.add(intent.getData()); - } - - return uris; - } - - private List getClipDataUris(Intent intent) { - List uris = new ArrayList<>(); - ClipData cd = intent.getClipData(); - if (cd != null) { - for (int i = 0; i < cd.getItemCount(); i++) { - uris.add(cd.getItemAt(i).getUri()); - } - } - return uris; - } } diff --git a/mage/src/main/java/mil/nga/giat/mage/profile/ProfilePictureViewerActivity.kt b/mage/src/main/java/mil/nga/giat/mage/profile/ProfilePictureViewerActivity.kt index 40be663ba..7f4251fed 100644 --- a/mage/src/main/java/mil/nga/giat/mage/profile/ProfilePictureViewerActivity.kt +++ b/mage/src/main/java/mil/nga/giat/mage/profile/ProfilePictureViewerActivity.kt @@ -13,27 +13,19 @@ import com.bumptech.glide.request.target.Target import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener +import dagger.hilt.android.AndroidEntryPoint import mil.nga.giat.mage.R import mil.nga.giat.mage.databinding.AttachmentViewerBinding import mil.nga.giat.mage.glide.GlideApp import mil.nga.giat.mage.glide.model.Avatar -import mil.nga.giat.mage.sdk.datastore.user.User -import mil.nga.giat.mage.sdk.datastore.user.UserHelper +import mil.nga.giat.mage.database.model.user.User +import mil.nga.giat.mage.data.datasource.user.UserLocalDataSource +import javax.inject.Inject +@AndroidEntryPoint class ProfilePictureViewerActivity : AppCompatActivity() { - companion object { - private val LOG_NAME = ProfilePictureViewerActivity::class.java.name - - private const val USER_ID_EXTRA = "USER_ID" - - fun intent(context: Context, user: User): Intent { - val intent = Intent(context, ProfilePictureViewerActivity::class.java) - intent.putExtra(USER_ID_EXTRA, user.id) - return intent - } - } - + @Inject lateinit var userLocalDataSource: UserLocalDataSource private lateinit var binding: AttachmentViewerBinding public override fun onCreate(savedInstanceState: Bundle?) { @@ -53,7 +45,7 @@ class ProfilePictureViewerActivity : AppCompatActivity() { try { val userID = intent.getLongExtra(USER_ID_EXTRA, -1) - val user = UserHelper.getInstance(applicationContext).read(userID) + val user = userLocalDataSource.read(userID) this.title = user.displayName GlideApp.with(this) @@ -91,4 +83,16 @@ class ProfilePictureViewerActivity : AppCompatActivity() { .create() .show() } + + companion object { + private val LOG_NAME = ProfilePictureViewerActivity::class.java.name + + private const val USER_ID_EXTRA = "USER_ID" + + fun intent(context: Context, user: User): Intent { + val intent = Intent(context, ProfilePictureViewerActivity::class.java) + intent.putExtra(USER_ID_EXTRA, user.id) + return intent + } + } } diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/Compatibility.kt b/mage/src/main/java/mil/nga/giat/mage/sdk/Compatibility.kt index 85031f74d..b672a5c84 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/Compatibility.kt +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/Compatibility.kt @@ -9,7 +9,7 @@ class Compatibility { data class Server(val major: Int, val minor: Int) companion object { - val servers = listOf( + private val servers = listOf( Server(5, 4), Server(6, 0) ) diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/ConnectivityAwareIntentService.java b/mage/src/main/java/mil/nga/giat/mage/sdk/ConnectivityAwareIntentService.java deleted file mode 100644 index 4efc87fa9..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/ConnectivityAwareIntentService.java +++ /dev/null @@ -1,68 +0,0 @@ -package mil.nga.giat.mage.sdk; - -import android.app.IntentService; -import android.content.Intent; - -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.connectivity.NetworkChangeReceiver; -import mil.nga.giat.mage.sdk.event.IConnectivityEventListener; - -public abstract class ConnectivityAwareIntentService extends IntentService implements IConnectivityEventListener { - - public ConnectivityAwareIntentService(String name) { - super(name); - } - - protected Boolean isConnected = Boolean.TRUE; - - protected boolean isCanceled = false; - - @Override - public void onError(Throwable error) { - - } - - @Override - public void onAllDisconnected() { - isConnected = Boolean.FALSE; - } - - @Override - public void onAnyConnected() { - isConnected = Boolean.TRUE; - } - - @Override - public void onWifiConnected() { - //if more granular connectivity management is ever needed. i.e. for attachments? - } - - @Override - public void onWifiDisconnected() { - //if more granular connectivity management is ever needed. i.e. for attachments? - } - - @Override - public void onMobileDataConnected() { - //if more granular connectivity management is ever needed. i.e. for attachments? - } - - @Override - public void onMobileDataDisconnected() { - //if more granular connectivity management is ever needed. i.e. for attachments? - } - - @Override - protected void onHandleIntent(Intent intent) { - //set up initial connection state - isConnected = ConnectivityUtility.isOnline(getApplicationContext()); - //enable connectivity event handling - NetworkChangeReceiver.getInstance().addListener(this); - } - - @Override - public void onDestroy() { - isCanceled = true; - super.onDestroy(); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/ConnectivityUtility.java b/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/ConnectivityUtility.java deleted file mode 100644 index 25cf0ccb4..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/ConnectivityUtility.java +++ /dev/null @@ -1,24 +0,0 @@ -package mil.nga.giat.mage.sdk.connectivity; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; - -/** - * Utility to deal with network connectivity. - * - */ -public class ConnectivityUtility { - - /** - * Used to check for connectivity - * - * @param context - * @return - */ - public static boolean isOnline(Context context) { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - return netInfo != null && netInfo.isConnectedOrConnecting(); - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/NetworkChangeReceiver.java b/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/NetworkChangeReceiver.java deleted file mode 100644 index 3d6001a79..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/connectivity/NetworkChangeReceiver.java +++ /dev/null @@ -1,187 +0,0 @@ -package mil.nga.giat.mage.sdk.connectivity; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.util.Log; - -import java.util.Collection; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import mil.nga.giat.mage.sdk.event.IConnectivityEventListener; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; - - -/** - * When network connectivity changes (lost or gained) notify the right listeners. - * When we gain connectivity, wait for a little bit to make sure we keep it before notifying listeners. - * - * @author wiedemanns - */ -public class NetworkChangeReceiver extends BroadcastReceiver implements IEventDispatcher { - - /** - * Singleton. - */ - private static NetworkChangeReceiver mNetworkChangeReceiver; - - /** - * Do not use! - */ - public NetworkChangeReceiver() { - - } - - public static NetworkChangeReceiver getInstance() { - if (mNetworkChangeReceiver == null) { - mNetworkChangeReceiver = new NetworkChangeReceiver(); - } - return mNetworkChangeReceiver; - } - - private static final int sleepDelay = 15; // in seconds - - private static final String LOG_NAME = NetworkChangeReceiver.class.getName(); - - private static final Collection listeners = new CopyOnWriteArrayList(); - - private static final ScheduledExecutorService connectionFutureWorker = Executors.newSingleThreadScheduledExecutor(); - private static ScheduledFuture connectionDataFuture = null; - private static Boolean oldConnectionAvailabilityState = null; - - private static final ScheduledExecutorService wifiFutureWorker = Executors.newSingleThreadScheduledExecutor(); - private static ScheduledFuture wifiFuture = null; - private static Boolean oldWifiAvailabilityState = null; - - private static final ScheduledExecutorService mobileFutureWorker = Executors.newSingleThreadScheduledExecutor(); - private static ScheduledFuture mobileDataFuture = null; - private static Boolean oldMobileDataAvailabilityState = null; - - @Override - public void onReceive(final Context context, final Intent intent) { - final ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - - boolean newWifiAvailabilityState = false; - final NetworkInfo wifi = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - if (wifi != null) { - newWifiAvailabilityState = wifi.isConnected(); - } - - boolean newMobileDataAvailabilityState = false; - final NetworkInfo mobile = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); - if (mobile != null) { - newMobileDataAvailabilityState = mobile.isConnected(); - } - - final boolean newConnectionAvailabilityState = newWifiAvailabilityState || newMobileDataAvailabilityState; - - // set the old state if it's the first time through! - if (oldWifiAvailabilityState == null) { - oldWifiAvailabilityState = !newWifiAvailabilityState; - } - - if (oldMobileDataAvailabilityState == null) { - oldMobileDataAvailabilityState = !newMobileDataAvailabilityState; - } - - if (oldConnectionAvailabilityState == null) { - oldConnectionAvailabilityState = !newConnectionAvailabilityState; - } - - // was there a change in wifi? - if (oldWifiAvailabilityState ^ newWifiAvailabilityState) { - // is wifi now on? - if(newWifiAvailabilityState) { - Runnable task = new Runnable() { - public void run() { - Log.d(LOG_NAME, "WIFI IS ON"); - for (IConnectivityEventListener listener : listeners) { - listener.onWifiConnected(); - } - } - }; - wifiFuture = wifiFutureWorker.schedule(task, sleepDelay, TimeUnit.SECONDS); - } else { - if(wifiFuture != null) { - wifiFuture.cancel(false); - wifiFuture = null; - } - Log.d(LOG_NAME, "WIFI IS OFF"); - for (IConnectivityEventListener listener : listeners) { - listener.onWifiDisconnected(); - } - } - } - - // was there a change in mobile data? - if (oldMobileDataAvailabilityState ^ newMobileDataAvailabilityState) { - // is mobile data now on? - if(newMobileDataAvailabilityState) { - Runnable task = new Runnable() { - public void run() { - Log.d(LOG_NAME, "MOBILE DATA IS ON"); - for (IConnectivityEventListener listener : listeners) { - listener.onMobileDataConnected(); - } - } - }; - mobileDataFuture = mobileFutureWorker.schedule(task, sleepDelay, TimeUnit.SECONDS); - } else { - if(mobileDataFuture != null) { - mobileDataFuture.cancel(false); - mobileDataFuture = null; - } - Log.d(LOG_NAME, "MOBILE DATA IS OFF"); - for (IConnectivityEventListener listener : listeners) { - listener.onMobileDataDisconnected(); - } - } - } - - // was there a change in general connectivity? - if (oldConnectionAvailabilityState ^ newConnectionAvailabilityState) { - // is mobile data now on? - if(newConnectionAvailabilityState) { - Runnable task = new Runnable() { - public void run() { - Log.d(LOG_NAME, "CONNECTIVITY IS ON"); - for (IConnectivityEventListener listener : listeners) { - listener.onAnyConnected(); - } - } - }; - connectionDataFuture = connectionFutureWorker.schedule(task, sleepDelay, TimeUnit.SECONDS); - } else { - if(connectionDataFuture != null) { - connectionDataFuture.cancel(false); - connectionDataFuture = null; - } - Log.d(LOG_NAME, "CONNECTIVITY IS OFF"); - for (IConnectivityEventListener listener : listeners) { - listener.onAllDisconnected(); - } - } - } - - // set the old states! - oldWifiAvailabilityState = newWifiAvailabilityState; - oldMobileDataAvailabilityState = newMobileDataAvailabilityState; - oldConnectionAvailabilityState = newConnectionAvailabilityState; - } - - @Override - public boolean addListener(IConnectivityEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(IConnectivityEventListener listener) { - return listeners.remove(listener); - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoHelper.java deleted file mode 100644 index 3626bae44..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoHelper.java +++ /dev/null @@ -1,17 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore; - -import android.content.Context; - -/** - * Abstract class that Helpers should extend - * - */ -public abstract class DaoHelper implements IDaoHelper { - protected final DaoStore daoStore; - protected final Context mApplicationContext; - - protected DaoHelper(Context pContext) { - daoStore = DaoStore.getInstance(pContext); - mApplicationContext = pContext; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoStore.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoStore.java deleted file mode 100644 index 387daff7c..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/DaoStore.java +++ /dev/null @@ -1,457 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; - -import com.j256.ormlite.android.apptools.OpenHelperManager; -import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper; -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.field.DataPersisterManager; -import com.j256.ormlite.support.ConnectionSource; -import com.j256.ormlite.table.TableUtils; - -import java.sql.SQLException; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; -import mil.nga.giat.mage.sdk.datastore.observation.Attachment; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationErrorClassPersister; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationFavorite; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationForm; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationImportant; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationProperty; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureProperty; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.Form; -import mil.nga.giat.mage.sdk.datastore.user.Role; -import mil.nga.giat.mage.sdk.datastore.user.Team; -import mil.nga.giat.mage.sdk.datastore.user.TeamEvent; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserLocal; -import mil.nga.giat.mage.sdk.datastore.user.UserTeam; - -/** - * This is an implementation of OrmLite android database Helper. Go here to get - * daos that you may need. Manage your table creation and update strategies here - * as well. - */ -public class DaoStore extends OrmLiteSqliteOpenHelper { - - private static DaoStore helperInstance; - - private static final String DATABASE_NAME = "mage.db"; - private static final String LOG_NAME = DaoStore.class.getName(); - // Making this public so we can check if it has been upgraded and log the user out - public static final int DATABASE_VERSION = 22; - - // Observation DAOS - private Dao observationDao; - private Dao observationFormDao; - private Dao observationPropertyDao; - private Dao observationImportantDao; - private Dao observationFavoriteDao; - private Dao attachmentDao; - - // User and Location DAOS - private Dao userDao; - private Dao roleDao; - private Dao eventDao; - private Dao formDao; - private Dao teamDao; - private Dao userLocalDao; - private Dao userTeamDao; - private Dao teamEventDao; - private Dao locationDao; - private Dao locationPropertyDao; - - // Layer and StaticFeature DAOS - private Dao layerDao; - private Dao staticFeatureDao; - private Dao staticFeaturePropertyDao; - - /** - * Singleton implementation. - * - * @param context context - * @return the dao store - */ - public static DaoStore getInstance(Context context) { - if (helperInstance == null) { - OpenHelperManager.setOpenHelperClass(DaoStore.class); - helperInstance = OpenHelperManager.getHelper(context, DaoStore.class); - } - - return helperInstance; - } - - /** - * Constructor that takes an android Context. - * - * @param context context - * - */ - public DaoStore(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - private void createTables() throws SQLException { - TableUtils.createTable(connectionSource, Observation.class); - TableUtils.createTable(connectionSource, ObservationForm.class); - TableUtils.createTable(connectionSource, ObservationProperty.class); - TableUtils.createTable(connectionSource, ObservationImportant.class); - TableUtils.createTable(connectionSource, ObservationFavorite.class); - TableUtils.createTable(connectionSource, Attachment.class); - - TableUtils.createTable(connectionSource, User.class); - TableUtils.createTable(connectionSource, UserLocal.class); - TableUtils.createTable(connectionSource, Role.class); - TableUtils.createTable(connectionSource, Event.class); - TableUtils.createTable(connectionSource, Form.class); - TableUtils.createTable(connectionSource, Team.class); - TableUtils.createTable(connectionSource, UserTeam.class); - TableUtils.createTable(connectionSource, TeamEvent.class); - TableUtils.createTable(connectionSource, Location.class); - TableUtils.createTable(connectionSource, LocationProperty.class); - - TableUtils.createTable(connectionSource, Layer.class); - TableUtils.createTable(connectionSource, StaticFeature.class); - TableUtils.createTable(connectionSource, StaticFeatureProperty.class); - - DataPersisterManager.registerDataPersisters(ObservationErrorClassPersister.getSingleton()); - } - - @Override - public void onCreate(SQLiteDatabase sqliteDatabase, ConnectionSource connectionSource) { - try { - createTables(); - } catch (SQLException se) { - Log.e(LOG_NAME, "Could not create tables.", se); - } - } - - private void dropTables() throws SQLException { - TableUtils.dropTable(connectionSource, Observation.class, Boolean.TRUE); - - TableUtils.dropTable(connectionSource, ObservationForm.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, ObservationProperty.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, ObservationImportant.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, ObservationFavorite.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, Attachment.class, Boolean.TRUE); - - TableUtils.dropTable(connectionSource, User.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, UserLocal.class, Boolean.TRUE); - - TableUtils.dropTable(connectionSource, Role.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, Event.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, Form.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, Team.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, UserTeam.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, TeamEvent.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, Location.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, LocationProperty.class, Boolean.TRUE); - - TableUtils.dropTable(connectionSource, Layer.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, StaticFeature.class, Boolean.TRUE); - TableUtils.dropTable(connectionSource, StaticFeatureProperty.class, Boolean.TRUE); - } - - @Override - public void onUpgrade(SQLiteDatabase database, ConnectionSource connectionSource, int oldVersion, int newVersion) { - resetDatabase(); - } - - /** - * Drop and create all tables. - */ - public void resetDatabase() { - try { - Log.d(LOG_NAME, "Reseting Database."); - dropTables(); - createTables(); - Log.d(LOG_NAME, "Reset Database."); - } catch (SQLException se) { - Log.e(LOG_NAME, "Could not reset Database.", se); - } - } - - @Override - public void close() { - super.close(); - - helperInstance = null; - - observationDao = null; - observationFormDao = null; - observationPropertyDao = null; - observationImportantDao = null; - observationFavoriteDao = null; - attachmentDao = null; - - userDao = null; - roleDao = null; - eventDao = null; - formDao = null; - teamDao = null; - userLocalDao = null; - userTeamDao = null; - teamEventDao = null; - locationDao = null; - locationPropertyDao = null; - - layerDao = null; - staticFeatureDao = null; - staticFeaturePropertyDao = null; - } - - /** - * Getter for the ObservationDao. - * - * @return This instance's ObservationDao - * @throws SQLException - */ - public Dao getObservationDao() throws SQLException { - if (observationDao == null) { - observationDao = getDao(Observation.class); - } - return observationDao; - } - - /** - * Getter for the FormDao - * - * @return This instance's PropertyDao - * @throws SQLException - */ - public Dao getObservationFormDao() throws SQLException { - if (observationFormDao == null) { - observationFormDao = getDao(ObservationForm.class); - } - return observationFormDao; - } - - /** - * Getter for the PropertyDao - * - * @return This instance's PropertyDao - * @throws SQLException - */ - public Dao getObservationPropertyDao() throws SQLException { - if (observationPropertyDao == null) { - observationPropertyDao = getDao(ObservationProperty.class); - } - return observationPropertyDao; - } - - /** - * Getter for the ObservationImportantDao - * - * @return This instance's ObservationImportantDao - * @throws SQLException - */ - public Dao getObservationImportantDao() throws SQLException { - if (observationImportantDao == null) { - observationImportantDao = getDao(ObservationImportant.class); - } - return observationImportantDao; - } - - /** - * Getter for the ObservationFavoriteDao - * - * @return This instance's ObservationFavoriteDao - * @throws SQLException - */ - public Dao getObservationFavoriteDao() throws SQLException { - if (observationFavoriteDao == null) { - observationFavoriteDao = getDao(ObservationFavorite.class); - } - return observationFavoriteDao; - } - - /** - * Getter for the AttachmentDao - * - * @return This instance's AttachmentDao - * @throws SQLException - */ - public Dao getAttachmentDao() throws SQLException { - if (attachmentDao == null) { - attachmentDao = getDao(Attachment.class); - } - return attachmentDao; - } - - /** - * Getter for the UserDao - * - * @return This instance's UserDao - * @throws SQLException - */ - public Dao getUserDao() throws SQLException { - if (userDao == null) { - userDao = getDao(User.class); - } - return userDao; - } - - /** - * Getter for the UserLocalDao - * - * @return This instance's UserLocalDao - * @throws SQLException - */ - public Dao getUserLocalDao() throws SQLException { - if (userLocalDao == null) { - userLocalDao = getDao(UserLocal.class); - } - return userLocalDao; - } - - - /** - * Getter for the RoleDao - * - * @return This instance's RoleDao - * @throws SQLException - */ - public Dao getRoleDao() throws SQLException { - if (roleDao == null) { - roleDao = getDao(Role.class); - } - return roleDao; - } - - /** - * Getter for the EventDao - * - * @return This instance's EventDao - * @throws SQLException - */ - public Dao getEventDao() throws SQLException { - if (eventDao == null) { - eventDao = getDao(Event.class); - } - return eventDao; - } - - /** - * Getter for the FormDao - * - * @return This instance's EventDao - * @throws SQLException - */ - public Dao getFormDao() throws SQLException { - if (formDao == null) { - formDao = getDao(Form.class); - } - return formDao; - } - - /** - * Getter for the TeamDao - * - * @return This instance's TeamDao - * @throws SQLException - */ - public Dao getTeamDao() throws SQLException { - if (teamDao == null) { - teamDao = getDao(Team.class); - } - return teamDao; - } - - /** - * Getter for the UserTeamDao - * - * @return This instance's UserTeamDao - * @throws SQLException - */ - public Dao getUserTeamDao() throws SQLException { - if (userTeamDao == null) { - userTeamDao = getDao(UserTeam.class); - } - return userTeamDao; - } - - /** - * Getter for the TeamEventDao - * - * @return This instance's TeamEventDao - * @throws SQLException - */ - public Dao getTeamEventDao() throws SQLException { - if (teamEventDao == null) { - teamEventDao = getDao(TeamEvent.class); - } - return teamEventDao; - } - - /** - * Getter for the LocationDao - * - * @return This instance's LocationDao - * @throws SQLException - */ - public Dao getLocationDao() throws SQLException { - if (locationDao == null) { - locationDao = getDao(Location.class); - } - return locationDao; - } - - /** - * Getter for the LocationPropertyDao - * - * @return This instance's LocationPropertyDao - * @throws SQLException - */ - public Dao getLocationPropertyDao() throws SQLException { - if (locationPropertyDao == null) { - locationPropertyDao = getDao(LocationProperty.class); - } - return locationPropertyDao; - } - - /** - * Getter for the LayerDao - * - * @return This instance's LayerDao - * @throws SQLException - */ - public Dao getLayerDao() throws SQLException { - if (layerDao == null) { - layerDao = getDao(Layer.class); - } - return layerDao; - } - - /** - * Getter for the StaticFeatureDao - * - * @return This instance's StaticFeatureDao - * @throws SQLException - */ - public Dao getStaticFeatureDao() throws SQLException { - if (staticFeatureDao == null) { - staticFeatureDao = getDao(StaticFeature.class); - } - return staticFeatureDao; - } - - /** - * Getter for the StaticFeaturePropertyDao - * - * @return This instance's StaticFeaturePropertyDao - * @throws SQLException - */ - public Dao getStaticFeaturePropertyDao() throws SQLException { - if (staticFeaturePropertyDao == null) { - staticFeaturePropertyDao = getDao(StaticFeatureProperty.class); - } - return staticFeaturePropertyDao; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/IDaoHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/IDaoHelper.java deleted file mode 100644 index 20dbee566..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/IDaoHelper.java +++ /dev/null @@ -1,17 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore; - -public interface IDaoHelper { - - T create(T pDao) throws Exception; - - T read(Long id) throws Exception; - - T read(String remoteId) throws Exception; - - // TODO : readAll - - T update(T pDao) throws Exception; - - // TODO : delete - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/LayerHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/LayerHelper.java deleted file mode 100644 index 80d20bb91..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/layer/LayerHelper.java +++ /dev/null @@ -1,232 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.layer; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.stmt.Where; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.ILayerEventListener; -import mil.nga.giat.mage.sdk.exceptions.LayerException; - -/** - * A utility class for accessing {@link Layer} data from the physical data - * model. The details of ORM DAOs and Lazy Loading should not be exposed past - * this class. - * - */ -public class LayerHelper extends DaoHelper implements IEventDispatcher { - - private static final String LOG_NAME = LayerHelper.class.getName(); - - private final Dao layerDao; - - private final Collection listeners = new CopyOnWriteArrayList(); - - /** - * Singleton. - */ - private static LayerHelper mLayerHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational LocationHelper. - */ - public static synchronized LayerHelper getInstance(Context context) { - if (mLayerHelper == null) { - mLayerHelper = new LayerHelper(context); - } - return mLayerHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param context - */ - private LayerHelper(Context context) { - super(context); - - try { - layerDao = daoStore.getLayerDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Layers database.", sqle); - - throw new IllegalStateException("Unable to communicate with Layers database.", sqle); - } - - } - - public List readAll(String type) throws LayerException { - List layers = new ArrayList<>(); - try { - layers = layerDao.queryBuilder().where().eq("type", type).query(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Layers", sqle); - throw new LayerException("Unable to read Layers.", sqle); - } - return layers; - } - - public List readByEvent(Event event, String type) throws LayerException { - List layers = new ArrayList<>(); - if (event == null) { - return layers; - } - try { - Where where = layerDao.queryBuilder().where().eq("event_id", event.getId()); - - if (type != null) { - where.and().eq("type", type); - } - - layers = where.query(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Layers", sqle); - throw new LayerException("Unable to read Layers.", sqle); - } - return layers; - } - - @Override - public Layer read(Long id) throws LayerException { - try { - return layerDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new LayerException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - @Override - public Layer read(String pRemoteId) throws LayerException { - Layer layer = null; - try { - List results = layerDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - layer = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new LayerException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - - return layer; - } - - @Override - public Layer create(Layer pLayer) throws LayerException { - - Layer createdLayer; - try { - createdLayer = layerDao.createIfNotExists(pLayer); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the layer: " + pLayer + ".", sqle); - throw new LayerException("There was a problem creating the layer: " + pLayer + ".", sqle); - } - // fire the event - for (ILayerEventListener listener : listeners) { - listener.onLayerCreated(pLayer); - } - - return createdLayer; - } - - @Override - public Layer update(Layer layer) throws LayerException { - try { - int count = layerDao.update(layer); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem updating layer: " + layer); - throw new LayerException("There was a problem updating layer: " + layer, sqle); - } - - for (ILayerEventListener listener : listeners) { - listener.onLayerUpdated(layer); - } - - return layer; - } - - public Layer getByRelativePath(String relativePath) throws LayerException { - Layer layer = null; - try { - List results = layerDao.queryBuilder().where().eq("relative_path", relativePath).query(); - if (results != null && results.size() > 0) { - layer = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for relativePath = '" + relativePath + "'", sqle); - throw new LayerException("Unable to query for existence for relativePath = '" + relativePath + "'", sqle); - } - - return layer; - } - - public Layer getByDownloadId(long downloadId) throws LayerException { - Layer layer = null; - try { - List results = layerDao.queryBuilder().where().eq("download_id", downloadId).query(); - if (results != null && results.size() > 0) { - layer = results.get(0); - } - } catch (SQLException sqle) { - throw new LayerException("Unable to query Layer by download id = '" + downloadId + "'", sqle); - } - - return layer; - } - - public void delete(Long pPrimaryKey) throws LayerException { - try { - Layer layer = layerDao.queryForId(pPrimaryKey); - - if(layer != null) { - StaticFeatureHelper.getInstance(mApplicationContext).deleteAll(layer.getId()); - - // finally, delete the Layer. - layerDao.deleteById(pPrimaryKey); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Unable to delete layer: " + pPrimaryKey, e); - throw new LayerException("Unable to delete layer: " + pPrimaryKey, e); - } - } - - public void deleteAll(String type) throws LayerException { - try { - Collection layers = layerDao.queryForAll(); - for (Layer layer : layers) { - if (type.equals(layer.getType())) { - delete(layer.getId()); - } - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Error deleting layers"); - } - } - - @Override - public boolean addListener(ILayerEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(ILayerEventListener listener) { - return listeners.remove(listener); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationHelper.java deleted file mode 100644 index 63e8b8f45..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/location/LocationHelper.java +++ /dev/null @@ -1,368 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.location; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.misc.TransactionManager; -import com.j256.ormlite.stmt.QueryBuilder; -import com.j256.ormlite.stmt.Where; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.ILocationEventListener; -import mil.nga.giat.mage.sdk.exceptions.LocationException; -import mil.nga.giat.mage.sdk.exceptions.UserException; - -/** - * A utility class for accessing {@link Location} data from the physical data - * model. The details of ORM DAOs and Lazy Loading should not be exposed past - * this class. - * - */ -public class LocationHelper extends DaoHelper implements IEventDispatcher { - - private static final String LOG_NAME = LocationHelper.class.getName(); - - private final Dao locationDao; - private final Dao locationPropertyDao; - - private final Collection listeners = new CopyOnWriteArrayList<>(); - - private final Context context; - - /** - * Singleton. - */ - private static LocationHelper mLocationHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational LocationHelper. - */ - public static LocationHelper getInstance(Context context) { - if (mLocationHelper == null) { - mLocationHelper = new LocationHelper(context); - } - return mLocationHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param context - */ - private LocationHelper(Context context) { - super(context); - this.context = context; - - try { - locationDao = daoStore.getLocationDao(); - locationPropertyDao = daoStore.getLocationPropertyDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Location database.", sqle); - - throw new IllegalStateException("Unable to communicate with Location database.", sqle); - } - - } - - @Override - public Location create(final Location pLocation) throws LocationException { - Location createdLocation; - - try { - createdLocation = TransactionManager.callInTransaction(DaoStore.getInstance(context).getConnectionSource(), new Callable() { - @Override - public Location call() throws Exception { - // create Location geometry. - Location createdLocation = locationDao.createIfNotExists(pLocation); - // create Location properties. - Collection locationProperties = pLocation.getProperties(); - if (locationProperties != null) { - for (LocationProperty locationProperty : locationProperties) { - locationProperty.setLocation(createdLocation); - locationPropertyDao.create(locationProperty); - } - } - for (ILocationEventListener listener : listeners) { - listener.onLocationCreated(Collections.singletonList(createdLocation)); - } - return createdLocation; - } - }); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the location: " + pLocation + ".", sqle); - throw new LocationException("There was a problem creating the location: " + pLocation + ".", sqle); - } - - return createdLocation; - } - - @Override - public Location read(Long id) throws LocationException { - try { - return locationDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new LocationException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - @Override - public Location read(String pRemoteId) throws LocationException { - Location location = null; - try { - List results = locationDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - location = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new LocationException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - - return location; - } - - /** - * We have to realign all the foreign ids so the update works correctly - * - * @param location - * @throws LocationException - */ - public Location update(final Location location) throws LocationException { - // set all the ids as needed - final Location pOldLocation = read(location.getId()); - - // do the update - try { - TransactionManager.callInTransaction(DaoStore.getInstance(context).getConnectionSource(), new Callable() { - @Override - public Void call() throws Exception { - - location.setId(pOldLocation.getId()); - - // FIXME : make this run faster? - for (LocationProperty lp : location.getProperties()) { - for (LocationProperty olp : pOldLocation.getProperties()) { - if (lp.getKey().equalsIgnoreCase(olp.getKey())) { - lp.setId(olp.getId()); - break; - } - } - } - - locationDao.update(location); - - Collection properties = location.getProperties(); - if (properties != null) { - for (LocationProperty property : properties) { - property.setLocation(location); - locationPropertyDao.createOrUpdate(property); - } - } - return null; - } - }); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem updating the location: " + location + ".", sqle); - throw new LocationException("There was a problem updating the location: " + location + ".", sqle); - } - - // fire the event - for (ILocationEventListener listener : listeners) { - listener.onLocationUpdated(location); - } - - return location; - } - - /** - * Light-weight query for testing the existence of a location in the local data-store. - * @param location The primary key of the passed in Location object is used for the query. - * @return - */ - public Boolean exists(Location location) { - - Boolean exists = Boolean.FALSE; - try { - List locations = - locationDao.queryBuilder().selectColumns("_id").limit(1L).where().eq("_id", location.getId()).query(); - if(locations != null && locations.size() > 0) { - exists = Boolean.TRUE; - } - } - catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for location = '" + location.getId() + "'", sqle); - } - - return exists; - } - - public List getCurrentUserLocations(long limit, boolean includeRemote) { - List locations = new ArrayList<>(); - User currentUser = null; - try { - currentUser = UserHelper.getInstance(context.getApplicationContext()).readCurrentUser(); - } catch (UserException e) { - e.printStackTrace(); - } - if (currentUser != null) { - locations = getUserLocations(currentUser.getId(), null, limit, includeRemote); - } - return locations; - } - - public List getUserLocations(Long userId, Long eventId, long limit, boolean includeRemote) { - List locations = new ArrayList<>(); - QueryBuilder queryBuilder = locationDao.queryBuilder(); - try { - if (limit > 0) { - queryBuilder.limit(limit); - // most recent first! - queryBuilder.orderBy("timestamp", false); - } - Where where = queryBuilder.where().eq("user_id", userId); - - if (eventId != null) { - where.and().eq("event_id", eventId); - } - - if (!includeRemote) { - where.and().isNull("remote_id"); - } - - locations = locationDao.query(queryBuilder.prepare()); - } catch (SQLException e) { - Log.e(LOG_NAME, "Could not get current users Locations."); - } - return locations; - } - - /** - * This will delete the user's location(s) that have remote_ids. Locations - * that do NOT have remote_ids have not been sync'ed w/ the server. - * - * @param userLocalId - * The user's local id - * @throws LocationException - */ - public int deleteUserLocations(String userLocalId, Boolean keepMostRecent, Event event) throws LocationException { - - int numberLocationsDeleted = 0; - - try { - // newest first - QueryBuilder qb = locationDao.queryBuilder().orderBy(Location.COLUMN_NAME_TIMESTAMP, false); - qb.where() - .eq(Location.COLUMN_NAME_USER_ID, userLocalId) - .and() - .eq(Location.COLUMN_NAME_EVENT_ID, event.getId()); - - List locations = qb.query(); - - // if we should keep the most recent record, then skip one record. - if (keepMostRecent) { - locations.remove(0); - } - - numberLocationsDeleted = delete(locations); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to delete user's locations", sqle); - throw new LocationException("Unable to delete user's locations", sqle); - } - return numberLocationsDeleted; - } - - /** - * This will delete all locations for an event. - * - * @param event - * The event to remove locations for - * @throws LocationException - */ - public void deleteLocations(Event event) throws LocationException { - Log.e(LOG_NAME, "Deleting locations for event " + event.getName()); - - try { - QueryBuilder qb = locationDao.queryBuilder(); - qb.where().eq("event_id", event.getId()); - List locations = qb.query(); - delete(locations); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to delete locations for an event", sqle); - throw new LocationException("Unable to delete locations for an event", sqle); - } - } - - /** - * Deletes locations. This will also delete a Location's child - * Properties and Geometry data. - * - * @param locations - * @throws LocationException - */ - public int delete(final Collection locations) throws LocationException { - List deletedLocations = new ArrayList<>(); - try { - deletedLocations = TransactionManager.callInTransaction(DaoStore.getInstance(context).getConnectionSource(), new Callable>() { - @Override - public List call() throws Exception { - // read the full Location in - List deletedLocations = new ArrayList<>(); - for(Location location : locations) { - // delete Location properties. - Collection properties = location.getProperties(); - if (properties != null) { - for (LocationProperty property : properties) { - locationPropertyDao.deleteById(property.getId()); - } - } - - // finally, delete the Location. - locationDao.deleteById(location.getId()); - deletedLocations.add(location); - } - return deletedLocations; - } - }); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to delete Location: " + Arrays.toString(locations.toArray()), sqle); - throw new LocationException("Unable to delete Location: " + Arrays.toString(locations.toArray()), sqle); - } finally { - for (ILocationEventListener listener : listeners) { - listener.onLocationDeleted(deletedLocations); - } - } - - return deletedLocations.size(); - } - - @Override - public boolean addListener(final ILocationEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(ILocationEventListener listener) { - return listeners.remove(listener); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/AttachmentHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/AttachmentHelper.java deleted file mode 100644 index dec38180a..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/AttachmentHelper.java +++ /dev/null @@ -1,198 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.observation; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.stmt.QueryBuilder; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.event.IAttachmentEventListener; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.exceptions.ObservationException; - -public class AttachmentHelper extends DaoHelper implements IEventDispatcher { - - private static final String LOG_NAME = AttachmentHelper.class.getName(); - - private final Dao attachmentDao; - - private final Collection listeners = new CopyOnWriteArrayList(); - - /** - * Singleton. - */ - private static AttachmentHelper attachmentHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational ObservationHelper. - */ - public static AttachmentHelper getInstance(Context context) { - if (attachmentHelper == null) { - attachmentHelper = new AttachmentHelper(context); - } - return attachmentHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param context - */ - private AttachmentHelper(Context context) { - super(context); - - try { - // Set up DAOs - attachmentDao = daoStore.getAttachmentDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Attachment database.", sqle); - - throw new IllegalStateException("Unable to communicate with Attachment database.", sqle); - } - } - - @Override - public Attachment read(Long id) throws Exception { - Attachment attachment; - try { - attachment = attachmentDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Attachment: " + id, sqle); - throw new ObservationException("Unable to read Attachment: " + id, sqle); - } - - return attachment; - } - - @Override - public Attachment read(String remoteId) throws Exception { - Attachment attachment = null; - try { - List results = attachmentDao.queryBuilder().where().eq("remote_id", remoteId).query(); - if (results != null && results.size() > 0) { - attachment = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Attachment: " + remoteId, sqle); - throw new ObservationException("Unable to read Attachment: " + remoteId, sqle); - } - - return attachment; - } - - @Override - public Attachment create(Attachment attachment) throws SQLException { - try { - Attachment oldAttachment = read(attachment.getId()); - if (oldAttachment != null && attachment.getLocalPath() == null) { - attachment.setLocalPath(oldAttachment.getLocalPath()); - } - } catch (Exception e) { - } - - attachmentDao.createOrUpdate(attachment); - - for (IAttachmentEventListener listener : listeners) { - listener.onAttachmentCreated(attachment); - } - - return attachment; - } - - /** - * Persist attachment to database. - * - * The localPath member will not be set to null (removed). Please use - * the {@link AttachmentHelper#removeLocalPath(Attachment)} method to - * set the localPath to null. - * - * @param attachment - * @return the attachment - * @throws SQLException - */ - @Override - public Attachment update(Attachment attachment) throws SQLException { - try { - Attachment oldAttachment = read(attachment.getId()); - if (oldAttachment != null && attachment.getLocalPath() == null) { - attachment.setLocalPath(oldAttachment.getLocalPath()); - } - } catch (Exception e) { - } - - attachmentDao.update(attachment); - - for (IAttachmentEventListener listener : listeners) { - listener.onAttachmentUpdated(attachment); - } - - return attachment; - } - - /** - * Deletes an Attachment. - * - * @param attachment - * @throws Exception - */ - public void delete(Attachment attachment) throws SQLException { - attachmentDao.deleteById(attachment.getId()); - - for (IAttachmentEventListener listener : listeners) { - listener.onAttachmentDeleted(attachment); - } - } - - /** - * A List of {@link Attachment} from the datastore that are dirty (i.e. - * should be synced with the server). - * - * @return - */ - public List getDirtyAttachments() { - QueryBuilder qb = attachmentDao.queryBuilder(); - - - QueryBuilder queryBuilder = attachmentDao.queryBuilder(); - List attachments = new ArrayList(); - - try { - long count = qb.countOf(); - - queryBuilder.where().eq("dirty", true); - attachments = attachmentDao.query(queryBuilder.prepare()); - } catch (SQLException e) { - // TODO Auto-generated catch block - Log.e(LOG_NAME, "Could not get dirty Attachments."); - } - return attachments; - } - - @Override - public boolean addListener(IAttachmentEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(IAttachmentEventListener listener) { - return listeners.remove(listener); - } - - public void uploadableAttachment(Attachment attachment) { - for (IAttachmentEventListener listener : listeners) { - listener.onAttachmentUploadable(attachment); - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationErrorClassPersister.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationErrorClassPersister.java deleted file mode 100644 index a9d6a0196..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationErrorClassPersister.java +++ /dev/null @@ -1,87 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.observation; - -import com.j256.ormlite.field.FieldType; -import com.j256.ormlite.field.SqlType; -import com.j256.ormlite.field.types.StringType; - -import org.json.JSONException; -import org.json.JSONObject; - -public class ObservationErrorClassPersister extends StringType { - - private static final String ERROR_STATUS_CODE_KEY = "ERROR_STATUS_CODE_KEY"; - private static final String ERROR_MESSAGE_KEY = "ERROR_MESSAGE_KEY"; - private static final String ERROR_DESCRIPTION_KEY = "ERROR_DESCRIPTION_KEY"; - - private static final ObservationErrorClassPersister INSTANCE = new ObservationErrorClassPersister(); - - private ObservationErrorClassPersister() { - super(SqlType.STRING, new Class[] { ObservationErrorClassPersister.class }); - } - - public static ObservationErrorClassPersister getSingleton() { - return INSTANCE; - } - - @Override - public Object javaToSqlArg(FieldType fieldType, Object javaObject) { - ObservationError error = (ObservationError) javaObject; - return error != null ? jsonFromError(error) : null; - } - - @Override - public Object sqlArgToJava(FieldType fieldType, Object sqlArg, int columnPos) { - return sqlArg != null ? errorFromJson((String) sqlArg) : null; - } - - private String jsonFromError(ObservationError error) { - if (error == null) { - return null; - } - - JSONObject jsonObject = new JSONObject(); - try { - Integer statusCode = error.getStatusCode(); - if (statusCode != null) { - jsonObject.put(ERROR_STATUS_CODE_KEY, error.getStatusCode()); - } - - String message = error.getMessage(); - if (message != null) { - jsonObject.put(ERROR_MESSAGE_KEY, error.getMessage()); - } - - String description = error.getDescription(); - if (description != null) { - jsonObject.put(ERROR_DESCRIPTION_KEY, error.getDescription()); - } - } catch (JSONException e) { - } - - return jsonObject.toString(); - } - - private ObservationError errorFromJson(String errorJson) { - ObservationError observationError = new ObservationError(); - try { - JSONObject jsonObject = new JSONObject(errorJson); - - if (jsonObject.has(ERROR_STATUS_CODE_KEY)) { - observationError.setStatusCode(jsonObject.getInt(ERROR_STATUS_CODE_KEY)); - } - - if (jsonObject.has(ERROR_MESSAGE_KEY)) { - observationError.setMessage(jsonObject.getString(ERROR_MESSAGE_KEY)); - } - - if (jsonObject.has(ERROR_DESCRIPTION_KEY)) { - observationError.setDescription(jsonObject.getString(ERROR_DESCRIPTION_KEY)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - - return observationError; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationHelper.java deleted file mode 100644 index d865653e2..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/ObservationHelper.java +++ /dev/null @@ -1,737 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.observation; - -import android.content.Context; -import android.util.Log; - -import com.google.common.collect.Sets; -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.stmt.QueryBuilder; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.Compatibility; -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.IObservationEventListener; -import mil.nga.giat.mage.sdk.exceptions.ObservationException; -import mil.nga.giat.mage.sdk.exceptions.UserException; - -/** - * A utility class for accessing {@link Observation} data from the physical data - * model. The details of ORM DAOs and Lazy Loading should not be exposed past - * this class. - * - */ -public class ObservationHelper extends DaoHelper implements IEventDispatcher { - - private static final String LOG_NAME = ObservationHelper.class.getName(); - - private final Dao observationDao; - private final Dao observationFormDao; - private final Dao observationPropertyDao; - private final Dao observationImportantDao; - private final Dao observationFavoriteDao; - - private final Collection listeners = new CopyOnWriteArrayList<>(); - - /** - * Singleton. - */ - private static ObservationHelper mObservationHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational ObservationHelper. - */ - public static ObservationHelper getInstance(Context context) { - if (mObservationHelper == null) { - mObservationHelper = new ObservationHelper(context); - } - return mObservationHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param pContext - */ - private ObservationHelper(Context pContext) { - super(pContext); - try { - // Set up DAOs - observationDao = daoStore.getObservationDao(); - observationFormDao = daoStore.getObservationFormDao(); - observationPropertyDao = daoStore.getObservationPropertyDao(); - observationImportantDao = daoStore.getObservationImportantDao(); - observationFavoriteDao = daoStore.getObservationFavoriteDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Observation database.", sqle); - - throw new IllegalStateException("Unable to communicate with Observation database.", sqle); - } - - } - - @Override - public Observation create(Observation observation) throws ObservationException { - return create(observation, true); - } - - public Observation create(final Observation observation, final Boolean sendUserNotifcations) throws ObservationException { - Observation savedObservation = null; - try { - savedObservation = observationDao.callBatchTasks(new Callable() { - @Override - public Observation call() throws Exception { - Observation createdObservation; - - // Now we try and create the Observation structure. - try { - // set last Modified - if (observation.getLastModified() == null) { - observation.setLastModified(new Date()); - } - - // create the Observation. - observationDao.create(observation); - - Collection forms = observation.getForms(); - if (forms != null) { - for (ObservationForm form : forms) { - form.setObservation(observation); - observationFormDao.create(form); - - // create Observation properties. - Collection properties = form.getProperties(); - if (properties != null) { - for (ObservationProperty property : properties) { - property.setObservationForm(form); - observationPropertyDao.create(property); - } - } - } - } - - // create Observation favorites. - Collection favorites = observation.getFavorites(); - if (favorites != null) { - for (ObservationFavorite favorite : favorites) { - favorite.setObservation(observation); - observationFavoriteDao.create(favorite); - } - } - - // create Observation attachments. - Collection attachments = observation.getAttachments(); - for (Attachment attachment : attachments) { - try { - attachment.setObservation(observation); - AttachmentHelper.getInstance(mApplicationContext).create(attachment); - } catch (Exception e) { - throw new ObservationException("There was a problem creating the observations attachment: " + attachment + ".", e); - } - } - - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the observation: " + observation + ".", sqle); - throw new ObservationException("There was a problem creating the observation: " + observation + ".", sqle); - } - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationCreated(Collections.singletonList(observation), sendUserNotifcations); - } - - return observation; - } - }); - - } catch (Exception e) { - e.printStackTrace(); - } - - return savedObservation; - } - - @Override - public Observation read(Long id) throws ObservationException { - try { - return observationDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new ObservationException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - @Override - public Observation read(String pRemoteId) throws ObservationException { - Observation observation = null; - try { - List results = observationDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - observation = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new ObservationException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - - return observation; - } - - /** - * We have to realign all the foreign ids so the update works correctly - * - * @param observation - * @throws ObservationException - */ - @Override - public Observation update(final Observation observation) throws ObservationException { - Log.i(LOG_NAME, "Updating observation w/ id: " + observation.getId()); - Observation updatedObservation; - try { - updatedObservation = observationDao.callBatchTasks(() -> { - // set all the ids as needed - Observation oldObservation = read(observation.getId()); - - // if the observation is dirty, set the last_modified date! - // FIXME this is a server property and should not be set by the client, - // investigate why we are setting this - if (observation.isDirty()) { - observation.setLastModified(new Date()); - } - - ObservationImportant important = observation.getImportant(); - ObservationImportant oldImportant = oldObservation.getImportant(); - if (oldImportant != null && oldImportant.isDirty()) { - observation.setImportant(oldImportant); - } else { - if (important != null) { - if (oldImportant != null) { - important.setId(oldImportant.getId()); - } - observationImportantDao.createOrUpdate(important); - } else { - if (oldImportant != null) { - observationImportantDao.deleteById(oldImportant.getId()); - } - } - } - - observationDao.update(observation); - - // TODO might not need to delete all forms/properties when server sets a unique form id - // Delete all forms for this observation and all properties - for (ObservationForm form : oldObservation.getForms()) { - for (ObservationProperty property : form.getProperties()) { - observationPropertyDao.deleteById(property.getId()); - } - - observationFormDao.deleteById(form.getId()); - } - - for (ObservationForm form : observation.getForms()) { - form.setObservation(observation); - observationFormDao.createOrUpdate(form); - - for (ObservationProperty property : form.getProperties()) { - property.setObservationForm(form); - observationPropertyDao.createOrUpdate(property); - } - } - - Map favorites = observation.getFavoritesMap(); - Map oldFavorites = oldObservation.getFavoritesMap(); - Collection commonFavorites = Sets.intersection(favorites.keySet(), oldFavorites.keySet()); - - // Map database ids from old properties to new properties - for (String favoriteKey : commonFavorites) { - favorites.get(favoriteKey).setId(oldFavorites.get(favoriteKey).getId()); - } - - for (ObservationFavorite favorite : favorites.values()) { - ObservationFavorite oldFavorite = oldFavorites.get(favorite.getUserId()); - // only update favorite if local is not dirty - if (oldFavorite == null || !oldFavorite.isDirty()) { - favorite.setObservation(observation); - observationFavoriteDao.createOrUpdate(favorite); - } - } - - // Remove any favorites that existed in the old observation but do not exist - // in the new observation. - for (String favorite : Sets.difference(oldFavorites.keySet(), favorites.keySet())) { - // Only delete favorites that are not dirty - if (!oldFavorites.get(favorite).isDirty()) { - observationFavoriteDao.deleteById(oldFavorites.get(favorite).getId()); - } - } - - Log.i(LOG_NAME, "Observation attachments " + observation.getAttachments().size()); - - for (Attachment oldAttachment : oldObservation.getAttachments()) { - if (oldAttachment.getRemoteId() != null) { - Attachment found = null; - for (Attachment attachment : observation.getAttachments()) { - if (oldAttachment.getRemoteId().equals(attachment.getRemoteId())) { - found = attachment; - attachment.setId(oldAttachment.getId()); - } - } - - // if no longer in attachments array response from server, remove it - if (!Compatibility.Companion.isServerVersion5(mApplicationContext)) { - if (found == null) { - AttachmentHelper.getInstance(mApplicationContext).delete(oldAttachment); - } - } - } - } - - for (Attachment attachment : observation.getAttachments()) { - try { - attachment.setObservation(observation); - AttachmentHelper.getInstance(mApplicationContext).create(attachment); - } catch (Exception e) { - throw new ObservationException("There was a problem creating/updating the observations attachment: " + attachment + ".", e); - } - } - - observationDao.refresh(observation); - - if (observation.getRemoteId() != null) { - for (Attachment attachment : observation.getAttachments()) { - if (attachment.isDirty()) { - AttachmentHelper.getInstance(mApplicationContext).uploadableAttachment(attachment); - } - } - } - - return observation; - }); - } catch (Exception e) { - Log.e(LOG_NAME, "There was a problem updating the observation: " + observation + ".", e); - throw new ObservationException("There was a problem updating the observation: " + observation + ".", e); - } - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(updatedObservation); - } - - return updatedObservation; - } - - - public Collection readAll() throws ObservationException { - ConcurrentSkipListSet observations = new ConcurrentSkipListSet(); - try { - observations.addAll(observationDao.queryForAll()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Observations", sqle); - throw new ObservationException("Unable to read Observations.", sqle); - } - return observations; - } - - /** - * Gets the latest last modified date. Used when fetching. - * - * @return - */ - public Date getLatestCleanLastModified(Context context, Event currentEvent) { - Date lastModifiedDate = new Date(0); - QueryBuilder queryBuilder = observationDao.queryBuilder(); - - try { - User currentUser = UserHelper.getInstance(context.getApplicationContext()).readCurrentUser(); - if (currentUser != null) { - queryBuilder.where().eq("dirty", Boolean.FALSE).and().ne("user_id", String.valueOf(currentUser.getRemoteId())).and().eq("event_id", currentEvent.getId()); - queryBuilder.orderBy("last_modified", false); - Observation o = observationDao.queryForFirst(queryBuilder.prepare()); - if (o != null) { - lastModifiedDate = o.getLastModified(); - } - } - } catch (SQLException se) { - Log.e(LOG_NAME, "Could not get last_modified date."); - } catch (UserException e) { - Log.e(LOG_NAME, "Could not get current user."); - } - - return lastModifiedDate; - } - - /** - * Gets a List of Observations from the datastore that are dirty (i.e. - * should be synced with the server). - * - * @return - */ - public List getDirty() { - QueryBuilder queryBuilder = observationDao.queryBuilder(); - List observations = new ArrayList<>(); - - try { - queryBuilder.where().eq("dirty", true); - observations = observationDao.query(queryBuilder.prepare()); - } catch (SQLException e) { - // TODO Auto-generated catch block - Log.e(LOG_NAME, "Could not get dirty Observations."); - } - return observations; - } - - /** - * Archive an Observation. This will remove the observation from the server - * - * @param observation - * @throws ObservationException - */ - public void archive(final Observation observation) throws ObservationException { - if (observation.getRemoteId() == null) { - // observation does not exist on the server yet, just remove it from the database - try { - observationDao.delete(observation); - } catch (SQLException e) { - throw new ObservationException("Unable to archive Observation: " + observation.getId(), e); - } - } else { - observation.setState(State.ARCHIVE); - observation.setDirty(true); - try { - observationDao.update(observation); - } catch (SQLException e) { - throw new ObservationException("Unable to archive Observation: " + observation.getId(), e); - } - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(observation); - } - } - } - - /** - * Deletes an Observation. This will also delete an Observation's child - * Attachments, child Properties and Geometry data. - * - * @param observation - * @throws ObservationException - */ - public void delete(final Observation observation) throws ObservationException { - try { - observationDao.callBatchTasks(new Callable() { - @Override - public Void call() throws Exception { - // delete Observation forms. - Collection forms = observation.getForms(); - if (forms != null) { - for (ObservationForm form : forms) { - // delete Observation properties. - Collection properties = form.getProperties(); - if (properties != null) { - for (ObservationProperty property : properties) { - observationPropertyDao.deleteById(property.getId()); - } - } - - observationFormDao.deleteById(form.getId()); - } - } - - // delete Observation favorites. - Collection favorites = observation.getFavorites(); - if (favorites != null) { - for (ObservationFavorite favorite : favorites) { - observationFavoriteDao.deleteById(favorite.getId()); - } - } - - // delete Observation attachments. - Collection attachments = observation.getAttachments(); - if (attachments != null) { - AttachmentHelper attachmentHelper = AttachmentHelper.getInstance(mApplicationContext); - for (Attachment attachment : attachments) { - attachmentHelper.delete(attachment); - } - } - - // delete important - ObservationImportant important = observation.getImportant(); - if (important != null) { - observationImportantDao.deleteById(important.getId()); - } - - // finally, delete the Observation. - observationDao.deleteById(observation.getId()); - - for (IObservationEventListener listener : listeners) { - listener.onObservationDeleted(observation); - } - - return null; - } - }); - } catch (Exception e) { - Log.e(LOG_NAME, "Unable to delete Observation: " + observation.getId(), e); - throw new ObservationException("Unable to delete Observation: " + observation.getId(), e); - } - } - - /** - * This will delete all observations for an event. - * - * @param event - * The event to remove locations for - * @throws ObservationException - */ - public void deleteObservations(Event event) throws ObservationException { - Log.e(LOG_NAME, "Deleting observations for event " + event.getName()); - - try { - QueryBuilder qb = observationDao.queryBuilder(); - qb.where().eq("event_id", event.getId()); - for (Observation observation : qb.query()) { - delete(observation); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to delete observations for an event", sqle); - throw new ObservationException("Unable to delete observations for an event", sqle); - } - } - - /** - * This will mark the observation as important - * - * @param observation The observation to mark as important - * - * @throws ObservationException - */ - public void addImportant(Observation observation) throws ObservationException { - ObservationImportant important = observation.getImportant(); - important.setImportant(true); - important.setDirty(true); - try { - observationImportantDao.createOrUpdate(important); - observationDao.update(observation); - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(observation); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to favorite observation", e); - throw new ObservationException("Unable to favorite observation", e); - } - } - - /** - * This will remove the important mark from an observation. - * - * @param observation The observation to unfavorite - * - * @throws ObservationException - */ - public void removeImportant(Observation observation) throws ObservationException { - try { - Collection importants = observationImportantDao.queryForAll(); - Log.i(LOG_NAME, "foo"); - } catch (SQLException e) { - e.printStackTrace(); - } - - ObservationImportant important = observation.getImportant(); - if (important != null) { - important.setImportant(false); - important.setDirty(true); - try { - observationImportantDao.update(important); - observationDao.refresh(observation); - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(observation); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to unfavorite observation", e); - throw new ObservationException("Unable to unfavorite observation", e); - } - } - } - - public void updateImportant(Observation observation) throws ObservationException { - ObservationImportant important = observation.getImportant(); - - try { - if (important.isImportant()) { - important.setDirty(Boolean.FALSE); - observation.setImportant(important); - observationImportantDao.update(important); - } else { - observationImportantDao.delete(important); - } - - // Update the observation so that the lastModified time is updated - observationDao.update(observation); - observationDao.refresh(observation); - - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(observation); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to update observation favorite", e); - throw new ObservationException("Unable to update observation favorite", e); - } - } - - /** - * This will favorite and observation for the user. - * - * @param observation The observation to favorite - * @param user The user that is favoriting the observation - * - * @throws ObservationException - */ - public void favoriteObservation(Observation observation, User user) throws ObservationException { - Map favoritesMap = observation.getFavoritesMap(); - ObservationFavorite favorite = favoritesMap.get(user.getRemoteId()); - if (favorite == null) { - favorite = new ObservationFavorite(user.getRemoteId(), true); - } - - favorite.setObservation(observation); - favorite.setFavorite(true); - favorite.setDirty(true); - try { - observationFavoriteDao.createOrUpdate(favorite); - observationDao.refresh(observation); - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(favorite.getObservation()); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to favorite observation", e); - throw new ObservationException("Unable to favorite observation", e); - } - } - - /** - * This will unfavorite and observation for the user. - * - * @param observation The observation to unfavorite - * @param user The user that is unfavoriting the observation - * - * @throws ObservationException - */ - public void unfavoriteObservation(Observation observation, User user) throws ObservationException { - Map favoritesMap = observation.getFavoritesMap(); - ObservationFavorite favorite = favoritesMap.get(user.getRemoteId()); - if (favorite != null) { - favorite.setFavorite(false); - favorite.setDirty(true); - try { - observationFavoriteDao.update(favorite); - observationDao.refresh(observation); - - // fire the event - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(favorite.getObservation()); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to unfavorite observation", e); - throw new ObservationException("Unable to unfavorite observation", e); - } - } - } - - public void updateFavorite(ObservationFavorite favorite) throws ObservationException { - try { - Observation observation = favorite.getObservation(); - - if (favorite.isFavorite()) { - favorite.setDirty(Boolean.FALSE); - observationFavoriteDao.update(favorite); - } else { - observationFavoriteDao.delete(favorite); - } - - // Update the observation so that the lastModified time is updated - observationDao.update(observation); - observationDao.refresh(observation); - - for (IObservationEventListener listener : listeners) { - listener.onObservationUpdated(observation); - } - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to update observation favorite", e); - throw new ObservationException("Unable to update observation favorite", e); - } - } - - /** - * A List of {@link ObservationImportant} from the datastore that are dirty (i.e. - * should be synced with the server). - * - * @return - */ - public List getDirtyImportant() throws ObservationException { - try { - QueryBuilder importantQb = observationImportantDao.queryBuilder(); - importantQb.where().eq("dirty", true); - - QueryBuilder observationQb = observationDao.queryBuilder(); - return observationQb.join(importantQb).query(); - } catch (SQLException e) { - // TODO Auto-generated catch block - Log.e(LOG_NAME, "Unable to get dirty observation favorites", e); - throw new ObservationException("Unable to get dirty observation favorites", e); - } - } - - /** - * A List of {@link ObservationProperty} from the datastore that are dirty (i.e. - * should be synced with the server). - * - * @return - */ - public List getDirtyFavorites() throws ObservationException { - try { - QueryBuilder queryBuilder = observationFavoriteDao.queryBuilder(); - queryBuilder.where().eq("dirty", true); - - return observationFavoriteDao.query(queryBuilder.prepare()); - } catch (SQLException e) { - // TODO Auto-generated catch block - Log.e(LOG_NAME, "Unable to get dirty observation favorites", e); - throw new ObservationException("Unable to get dirty observation favorites", e); - } - } - - @Override - public boolean addListener(final IObservationEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(IObservationEventListener listener) { - return listeners.remove(listener); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/State.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/State.java deleted file mode 100644 index 65ba590a9..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/observation/State.java +++ /dev/null @@ -1,8 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.observation; - -/** - * The State of an {@link Observation} - */ -public enum State { - ACTIVE, COMPLETE, ARCHIVE -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureHelper.java deleted file mode 100644 index d7ce43bf3..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/staticfeature/StaticFeatureHelper.java +++ /dev/null @@ -1,246 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.staticfeature; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.misc.TransactionManager; -import com.j256.ormlite.stmt.DeleteBuilder; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.IStaticFeatureEventListener; -import mil.nga.giat.mage.sdk.exceptions.StaticFeatureException; - -public class StaticFeatureHelper extends DaoHelper implements IEventDispatcher { - - private static final String LOG_NAME = StaticFeatureHelper.class.getName(); - - private final Context context; - - private final Dao staticFeatureDao; - private final Dao staticFeaturePropertyDao; - - private final Collection listeners = new CopyOnWriteArrayList(); - - /** - * Singleton. - */ - private static StaticFeatureHelper mStaticFeatureHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational StaticFeatureHelper. - */ - public static StaticFeatureHelper getInstance(Context context) { - if (mStaticFeatureHelper == null) { - mStaticFeatureHelper = new StaticFeatureHelper(context); - } - return mStaticFeatureHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param context - */ - private StaticFeatureHelper(Context context) { - super(context); - this.context = context; - - try { - // Set up DAOs - staticFeatureDao = daoStore.getStaticFeatureDao(); - staticFeaturePropertyDao = daoStore.getStaticFeaturePropertyDao(); - - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with StaticFeature database.", sqle); - - throw new IllegalStateException("Unable to communicate with StaticFeature database.", sqle); - } - } - - @Override - public StaticFeature create(StaticFeature pStaticFeature) throws StaticFeatureException { - - StaticFeature createdStaticFeature; - try { - createdStaticFeature = staticFeatureDao.createIfNotExists(pStaticFeature); - // create Static Feature properties. - Collection properties = pStaticFeature.getProperties(); - if (properties != null) { - for (StaticFeatureProperty property : properties) { - property.setStaticFeature(createdStaticFeature); - staticFeaturePropertyDao.create(property); - } - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the static feature: " + pStaticFeature + ".", sqle); - throw new StaticFeatureException("There was a problem creating the static feature: " + pStaticFeature + ".", sqle); - } - - return createdStaticFeature; - } - - @Override - public StaticFeature update(StaticFeature pStaticFeature) throws Exception { - throw new UnsupportedOperationException(); - } - - /** - * Set of layers that features were added to, or already belonged to. - * - * @param staticFeatures - * @return - * @throws StaticFeatureException - */ - public Layer createAll(final Collection staticFeatures, final Layer pLayer) throws StaticFeatureException { - - try { - TransactionManager.callInTransaction(DaoStore.getInstance(context).getConnectionSource(), new Callable() { - @Override - public Void call() throws Exception { - for (StaticFeature staticFeature : staticFeatures) { - try { - Collection properties = staticFeature.getProperties(); - staticFeature = staticFeatureDao.createIfNotExists(staticFeature); - - // create Static Feature properties. - if (properties != null) { - for (StaticFeatureProperty property : properties) { - property.setStaticFeature(staticFeature); - staticFeaturePropertyDao.create(property); - } - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the static feature: " + staticFeature + ".", sqle); - continue; - // TODO Throw exception? - } - } - - return null; - } - }); - pLayer.setLoaded(true); - // fire the event - for (IStaticFeatureEventListener listener : listeners) { - listener.onStaticFeaturesCreated(pLayer); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating static features.", sqle); - } - - return pLayer; - } - - @Override - public StaticFeature read(Long id) throws StaticFeatureException { - try { - return staticFeatureDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new StaticFeatureException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - @Override - public StaticFeature read(String pRemoteId) throws StaticFeatureException { - StaticFeature staticFeature = null; - try { - List results = staticFeatureDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - staticFeature = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new StaticFeatureException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - - return staticFeature; - } - - public List readAll(Long pLayerId) throws StaticFeatureException { - List staticFeatures = new ArrayList(); - try { - List results = staticFeatureDao.queryBuilder().where().eq("layer_id", pLayerId).query(); - if (results != null) { - staticFeatures.addAll(results); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for features with layer id = '" + pLayerId + "'", sqle); - throw new StaticFeatureException("Unable to query for features with layer id = '" + pLayerId + "'", sqle); - } - - return staticFeatures; - } - - public StaticFeature readFeature(Long layerId, Long id) throws StaticFeatureException { - StaticFeature staticFeature = null; - try { - List results = staticFeatureDao.queryBuilder() - .where() - .eq(StaticFeature.STATIC_FEATURE_LAYER_ID, layerId) - .and() - .eq(StaticFeature.STATIC_FEATURE_ID, id) - .query(); - - if (results != null && results.size() > 0) { - staticFeature = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for feature with layer id: " + layerId + " and id: " + id, sqle); - throw new StaticFeatureException("Unable to query for feature with layer id: " + layerId + " and id: " + id, sqle); - } - - return staticFeature; - } - - public void deleteAll(Long layerId) throws StaticFeatureException { - List features = readAll(layerId); - Collection ids = new ArrayList<>(features.size()); - for (StaticFeature feature : features) { - ids.add(feature.getId()); - } - - try { - // Delete the properties (children) - DeleteBuilder propertyDeleteBuilder = staticFeaturePropertyDao.deleteBuilder(); - propertyDeleteBuilder.where().in(StaticFeatureProperty.STATIC_FEATURE_ID, ids); - int propertiesDeleted = staticFeaturePropertyDao.delete(propertyDeleteBuilder.prepare()); - Log.i(LOG_NAME, propertiesDeleted + " static feature properties deleted"); - - // All children deleted, delete the static feature. - DeleteBuilder featureDeleteBuilder = staticFeatureDao.deleteBuilder(); - featureDeleteBuilder.where().eq(StaticFeature.STATIC_FEATURE_LAYER_ID, layerId); - int featuresDeleted = staticFeatureDao.delete(featureDeleteBuilder.prepare()); - Log.i(LOG_NAME, featureDeleteBuilder + " features deleted"); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to delete Static Feature: " + ids, sqle); - throw new StaticFeatureException("Unable to delete Static Feature: " + ids, sqle); - } - } - - @Override - public boolean addListener(IStaticFeatureEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(IStaticFeatureEventListener listener) { - return listeners.remove(listener); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/EventHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/EventHelper.java deleted file mode 100644 index 16ff11d60..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/EventHelper.java +++ /dev/null @@ -1,263 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.user; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.misc.TransactionManager; -import com.j256.ormlite.stmt.DeleteBuilder; - -import org.apache.commons.lang3.StringUtils; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Callable; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.datastore.DaoStore; -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper; -import mil.nga.giat.mage.sdk.datastore.observation.ObservationHelper; -import mil.nga.giat.mage.sdk.exceptions.EventException; -import mil.nga.giat.mage.sdk.exceptions.UserException; - -/** - * A utility class for accessing {@link Event} data from the physical data model. - * The details of ORM DAOs and Lazy Loading should not be exposed past this - * class. - */ -public class EventHelper extends DaoHelper { - - private static final String LOG_NAME = EventHelper.class.getName(); - - private final Dao eventDao; - private final Dao formDao; - private final Dao teamEventDao; - - /** - * Singleton. - */ - private static EventHelper mEventHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational UserHelper. - */ - public static EventHelper getInstance(Context context) { - if (mEventHelper == null) { - mEventHelper = new EventHelper(context); - } - return mEventHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param pContext context - */ - private EventHelper(Context pContext) { - super(pContext); - - try { - eventDao = daoStore.getEventDao(); - formDao = daoStore.getFormDao(); - teamEventDao = daoStore.getTeamEventDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Event database.", sqle); - - throw new IllegalStateException("Unable to communicate with Event database.", sqle); - } - - } - - @Override - public Event create(Event pEvent) throws EventException { - Event createdEvent; - try { - createdEvent = eventDao.createIfNotExists(pEvent); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating event: " + pEvent, sqle); - throw new EventException("There was a problem creating event: " + pEvent, sqle); - } - return createdEvent; - } - - @Override - public Event read(Long id) throws EventException { - try { - return eventDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new EventException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - public List readAll() throws EventException { - List events = new ArrayList<>(); - try { - events.addAll(eventDao.queryForAll()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Events", sqle); - throw new EventException("Unable to read Events.", sqle); - } - return events; - } - - @Override - public Event read(String pRemoteId) throws EventException { - Event event = null; - try { - List results = eventDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - event = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new EventException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - return event; - } - - - @Override - public Event update(Event event) throws EventException { - try { - TransactionManager.callInTransaction(DaoStore.getInstance(mApplicationContext).getConnectionSource(), (Callable) () -> { - DeleteBuilder deleteBuilder = formDao.deleteBuilder(); - deleteBuilder.where().eq(Form.Companion.getColumnNameEventId(), event.getId()); - deleteBuilder.delete(); - - eventDao.update(event); - - for (Form form : event.getForms()) { - form.event = event; - formDao.create(form); - } - - return null; - }); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating event: " + event); - throw new EventException("There was a problem creating event: " + event, sqle); - } - return event; - } - - public Event createOrUpdate(Event event) { - try { - Event oldEvent = read(event.getRemoteId()); - if (oldEvent == null) { - event = create(event); - - for (Form form : event.getForms()) { - form.event = event; - formDao.create(form); - } - Log.d(LOG_NAME, "Created event with remote_id " + event.getRemoteId()); - } else { - event.setId(oldEvent.getId()); - update(event); - Log.d(LOG_NAME, "Updated event with remote_id " + event.getRemoteId()); - } - } catch (Exception e) { - Log.e(LOG_NAME, "There was a problem reading user: " + event, e); - } - return event; - } - - public Form getForm(Long formId) { - Form form = null; - try { - List forms = formDao.queryBuilder() - .where() - .eq("formId", formId) - .query(); - - if (forms != null && forms.size() > 0) { - form = forms.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Error pulling form with id: " + formId, sqle); - } - - return form; - } - - public Event getCurrentEvent() { - Event event = null; - try { - User user = UserHelper.getInstance(mApplicationContext).readCurrentUser(); - if (user != null) { - event = user.getUserLocal().getCurrentEvent(); - } else { - Log.d(LOG_NAME, "Current user is null. Why?"); - } - } catch(UserException ue) { - Log.e(LOG_NAME, "There is no current user. ", ue); - } - - return event; - } - - public List getRecentEvents() throws EventException { - List events = new ArrayList<>(); - try { - User user = UserHelper.getInstance(mApplicationContext).readCurrentUser(); - if (user != null) { - List recentEventIds = user.getRecentEventIds(); - List cases = new ArrayList<>(recentEventIds.size()); - for (int i = 0; i < recentEventIds.size(); i++) { - cases.add("WHEN " + recentEventIds.get(i) + " THEN " + i); - } - - events = eventDao - .queryBuilder() - .orderByRaw(String.format("CASE %s %s END", Event.COLUMN_NAME_REMOTE_ID, StringUtils.join(cases, " "))) - .where() - .in(Event.COLUMN_NAME_REMOTE_ID, user.getRecentEventIds()) - .query(); - } else { - Log.d(LOG_NAME, "Current user is null."); - } - } catch(Exception e) { - Log.e(LOG_NAME, "There was a problem reading users current event", e); - } - - return events; - } - - - /** - * Remove any events from the database that are not in this event list. - * - * @param remoteEvents list of events that should remain in the database, all others will be removed - */ - public void syncEvents(Set remoteEvents) { - try { - List eventsToRemove = readAll(); - eventsToRemove.removeAll(remoteEvents); - - for (Event eventToRemove : eventsToRemove) { - Log.e(LOG_NAME, "Removing event " + eventToRemove.getName()); - - LocationHelper.getInstance(mApplicationContext).deleteLocations(eventToRemove); - ObservationHelper.getInstance(mApplicationContext).deleteObservations(eventToRemove); - - DeleteBuilder teamDeleteBuilder = teamEventDao.deleteBuilder(); - teamDeleteBuilder.where().eq("event_id", eventToRemove.getId()); - teamDeleteBuilder.delete(); - - DeleteBuilder eventDeleteBuilder = eventDao.deleteBuilder(); - eventDeleteBuilder.where().idEq(eventToRemove.getId()); - eventDeleteBuilder.delete(); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Error deleting event ", e); - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permissions.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permissions.java deleted file mode 100644 index 71ab55c89..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/Permissions.java +++ /dev/null @@ -1,30 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.user; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; - -public class Permissions implements Serializable { - - private static final long serialVersionUID = -1912604919150929355L; - - private Collection permissions = new ArrayList<>(); - - public Permissions() { - - } - - public Permissions(Collection permissions) { - super(); - this.permissions = permissions; - } - - public Collection getPermissions() { - return permissions; - } - - public void setPermissions(Collection permissions) { - this.permissions = permissions; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/RoleHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/RoleHelper.java deleted file mode 100644 index 88ef7006b..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/RoleHelper.java +++ /dev/null @@ -1,145 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.user; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.exceptions.RoleException; - -/** - * - * A utility class for accessing {@link Role} data from the physical data model. - * The details of ORM DAOs and Lazy Loading should not be exposed past this - * class. - * - */ -public class RoleHelper extends DaoHelper { - - private static final String LOG_NAME = RoleHelper.class.getName(); - - private final Dao roleDao; - - /** - * Singleton. - */ - private static RoleHelper mRoleHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational {@link RoleHelper}. - */ - public static RoleHelper getInstance(Context context) { - if (mRoleHelper == null) { - mRoleHelper = new RoleHelper(context); - } - return mRoleHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param pContext - */ - private RoleHelper(Context pContext) { - super(pContext); - - try { - roleDao = daoStore.getRoleDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Role database.", sqle); - - throw new IllegalStateException("Unable to communicate with Role database.", sqle); - } - - } - - @Override - public Role create(Role pRole) throws RoleException { - Role createdRole = null; - try { - createdRole = roleDao.createIfNotExists(pRole); - Log.d(LOG_NAME, "created role with remote_id " + createdRole.getRemoteId()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating the role: " + pRole); - throw new RoleException("There was a problem creating the role: " + pRole, sqle); - } - return createdRole; - } - - public Role update(Role pRole) throws RoleException { - try { - roleDao.update(pRole); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating role: " + pRole); - throw new RoleException("There was a problem creating role: " + pRole, sqle); - } - return pRole; - } - - public Role createOrUpdate(Role role) { - try { - Role oldRole = read(role.getRemoteId()); - if (oldRole == null) { - role = create(role); - Log.d(LOG_NAME, "Created role with remote_id " + role.getRemoteId()); - } else { - // perform update? - role.setId(oldRole.getId()); - update(role); - Log.d(LOG_NAME, "Updated role with remote_id " + role.getRemoteId()); - } - } catch (RoleException re) { - Log.e(LOG_NAME, "There was a problem reading role: " + role, re); - } - return role; - } - - @Override - public Role read(Long id) throws RoleException { - try { - return roleDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new RoleException("Unable to query for existence for id = '" + id + "'", sqle); - } - - } - - @Override - public Role read(String pRemoteId) throws RoleException { - Role role = null; - try { - List results = roleDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - role = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new RoleException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - return role; - } - - public Collection readAll() throws RoleException { - Collection roles = new ArrayList<>(); - try { - roles.addAll(roleDao.queryForAll()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Observations", sqle); - throw new RoleException("Unable to read Roles.", sqle); - } - - return roles; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamHelper.java deleted file mode 100644 index 2dc5aa0af..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/TeamHelper.java +++ /dev/null @@ -1,240 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.user; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.stmt.DeleteBuilder; -import com.j256.ormlite.stmt.QueryBuilder; -import com.j256.ormlite.stmt.Where; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.exceptions.EventException; -import mil.nga.giat.mage.sdk.exceptions.TeamException; - -/** - * A utility class for accessing {@link Team} data from the physical data model. - * The details of ORM DAOs and Lazy Loading should not be exposed past this - * class. - */ -public class TeamHelper extends DaoHelper { - - private static final String LOG_NAME = TeamHelper.class.getName(); - - private final Dao teamDao; - private final Dao userTeamDao; - private final Dao teamEventDao; - - /** - * Singleton. - */ - private static TeamHelper mTeamHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational UserHelper. - */ - public static TeamHelper getInstance(Context context) { - if (mTeamHelper == null) { - mTeamHelper = new TeamHelper(context); - } - return mTeamHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param pContext context - * @param pContext context - */ - private TeamHelper(Context pContext) { - super(pContext); - - try { - teamDao = daoStore.getTeamDao(); - userTeamDao = daoStore.getUserTeamDao(); - teamEventDao = daoStore.getTeamEventDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with Team database.", sqle); - - throw new IllegalStateException("Unable to communicate with Team database.", sqle); - } - - } - - @Override - public Team create(Team pTeam) throws TeamException { - Team createdTeam; - try { - createdTeam = teamDao.createIfNotExists(pTeam); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating team: " + pTeam, sqle); - throw new TeamException("There was a problem creating team: " + pTeam, sqle); - } - - return createdTeam; - } - - @Override - public Team read(Long id) throws TeamException { - try { - return teamDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new TeamException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - public List readAll() throws EventException { - List teams = new ArrayList<>(); - try { - teams.addAll(teamDao.queryForAll()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to read Teams", sqle); - throw new EventException("Unable to read Teams.", sqle); - } - return teams; - } - - @Override - public Team read(String pRemoteId) throws TeamException { - Team team = null; - try { - List results = teamDao.queryBuilder().where().eq("remote_id", pRemoteId).query(); - if (results != null && results.size() > 0) { - team = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - throw new TeamException("Unable to query for existence for remote_id = '" + pRemoteId + "'", sqle); - } - return team; - } - - @Override - public Team update(Team pTeam) throws TeamException { - try { - teamDao.update(pTeam); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating team: " + pTeam); - throw new TeamException("There was a problem creating team: " + pTeam, sqle); - } - return pTeam; - } - - public Team createOrUpdate(Team team) { - try { - Team oldTeam = read(team.getRemoteId()); - if (oldTeam == null) { - team = create(team); - Log.d(LOG_NAME, "Created team with remote_id " + team.getRemoteId()); - } else { - // perform update? - team.setId(oldTeam.getId()); - update(team); - Log.d(LOG_NAME, "Updated team with remote_id " + team.getRemoteId()); - } - } catch (TeamException te) { - Log.e(LOG_NAME, "There was a problem reading team: " + team, te); - } - return team; - } - - public void deleteTeamEvents() { - try { - DeleteBuilder db = teamEventDao.deleteBuilder(); - db.delete(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem deleting teamevents.", sqle); - } - } - - public TeamEvent create(TeamEvent pTeamEvent) { - TeamEvent createdTeamEvent = null; - try { - createdTeamEvent = teamEventDao.createIfNotExists(pTeamEvent); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating teamevent: " + pTeamEvent, sqle); - } - return createdTeamEvent; - } - - public List getTeamsByUser(User pUser) { - List teams = new ArrayList<>(); - try { - QueryBuilder userTeamQuery = userTeamDao.queryBuilder(); - userTeamQuery.selectColumns("team_id"); - Where where = userTeamQuery.where(); - where.eq("user_id", pUser.getId()); - - QueryBuilder teamQuery = teamDao.queryBuilder(); - teamQuery.where().in("_id", userTeamQuery); - - teams = teamQuery.query(); - if(teams == null) { - teams = new ArrayList<>(); - } - - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem getting teams for the user: " + pUser, sqle); - } - return teams; - } - - public List getTeamsByEvent(Event pEvent) { - List teams = new ArrayList<>(); - try { - QueryBuilder teamEventQuery = teamEventDao.queryBuilder(); - teamEventQuery.selectColumns("team_id"); - Where where = teamEventQuery.where(); - where.eq("event_id", pEvent.getId()); - - QueryBuilder teamQuery = teamDao.queryBuilder(); - teamQuery.where().in("_id", teamEventQuery); - - teams = teamQuery.query(); - if(teams == null) { - teams = new ArrayList<>(); - } - - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem getting teams for the event: " + pEvent, sqle); - } - return teams; - } - - /** - * Remove any teams from the database that are not in this team list. - * - * @param remoteTeams list of team that should remain in the database, all others will be removed - */ - public void syncTeams(Set remoteTeams) { - try { - List teamsToRemove = readAll(); - teamsToRemove.removeAll(remoteTeams); - - for (Team teamToRemove : teamsToRemove) { - Log.e(LOG_NAME, "Removing team " + teamToRemove.getName()); - - DeleteBuilder teamDeleteBuilder = teamEventDao.deleteBuilder(); - teamDeleteBuilder.where().eq("team_id", teamToRemove.getId()); - teamDeleteBuilder.delete(); - - DeleteBuilder eventDeleteBuilder = teamDao.deleteBuilder(); - eventDeleteBuilder.where().idEq(teamToRemove.getId()); - eventDeleteBuilder.delete(); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Error deleting event ", e); - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserHelper.java b/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserHelper.java deleted file mode 100644 index d6be6449f..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/datastore/user/UserHelper.java +++ /dev/null @@ -1,416 +0,0 @@ -package mil.nga.giat.mage.sdk.datastore.user; - -import android.content.Context; -import android.util.Log; - -import com.j256.ormlite.dao.Dao; -import com.j256.ormlite.stmt.DeleteBuilder; -import com.j256.ormlite.stmt.PreparedQuery; -import com.j256.ormlite.stmt.QueryBuilder; -import com.j256.ormlite.stmt.UpdateBuilder; -import com.j256.ormlite.stmt.Where; - -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.datastore.DaoHelper; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.IEventEventListener; -import mil.nga.giat.mage.sdk.event.IUserDispatcher; -import mil.nga.giat.mage.sdk.event.IUserEventListener; -import mil.nga.giat.mage.sdk.exceptions.UserException; - -/** - * A utility class for accessing {@link User} data from the physical data model. - * The details of ORM DAOs and Lazy Loading should not be exposed past this - * class. - */ -public class UserHelper extends DaoHelper implements IEventDispatcher, IUserDispatcher { - - private static final String LOG_NAME = UserHelper.class.getName(); - - private final Dao userDao; - private final Dao userLocalDao; - private final Dao userTeamDao; - private final Dao teamEventDao; - - private static final Collection userListeners = new CopyOnWriteArrayList<>(); - private static final Collection eventListeners = new CopyOnWriteArrayList<>(); - - /** - * Singleton. - */ - private static UserHelper mUserHelper; - - /** - * Use of a Singleton here ensures that an excessive amount of DAOs are not - * created. - * - * @param context - * Application Context - * @return A fully constructed and operational UserHelper. - */ - public static UserHelper getInstance(Context context) { - if (mUserHelper == null) { - mUserHelper = new UserHelper(context); - } - return mUserHelper; - } - - /** - * Only one-per JVM. Singleton. - * - * @param context context - */ - private UserHelper(Context context) { - super(context); - - try { - userDao = daoStore.getUserDao(); - userLocalDao = daoStore.getUserLocalDao(); - userTeamDao = daoStore.getUserTeamDao(); - teamEventDao = daoStore.getTeamEventDao(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to communicate with User database.", sqle); - - throw new IllegalStateException("Unable to communicate with User database.", sqle); - } - } - - // FIXME : should add user to team if needed - @Override - public User create(User user) throws UserException { - User createdUser; - try { - UserLocal userLocal = userLocalDao.createIfNotExists(new UserLocal()); - user.setUserLocal(userLocal); - createdUser = userDao.createIfNotExists(user); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating user: " + user, sqle); - throw new UserException("There was a problem creating user: " + user, sqle); - } - - for (IUserEventListener listener : userListeners) { - listener.onUserCreated(createdUser); - } - - return createdUser; - } - - @Override - public User read(Long id) throws UserException { - try { - return userDao.queryForId(id); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for id = '" + id + "'", sqle); - throw new UserException("Unable to query for existence for id = '" + id + "'", sqle); - } - } - - @Override - public User read(String remoteId) throws UserException { - User user = null; - try { - List results = userDao.queryBuilder().where().eq("remote_id", remoteId).query(); - if (results != null && results.size() > 0) { - user = results.get(0); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_id = '" + remoteId + "'", sqle); - throw new UserException("Unable to query for existence for remote_id = '" + remoteId + "'", sqle); - } - return user; - } - - public List read(Collection remoteIds) throws UserException { - try { - return userDao.queryBuilder().where().in("remote_id", remoteIds).query(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to query for existence for remote_ids = '" + remoteIds + "'", sqle); - throw new UserException("Unable to query for existence for remote_ids = '" + remoteIds.toString() + "'", sqle); - } - } - - public User readCurrentUser() throws UserException { - User user; - - try { - QueryBuilder userLocalQuery = userLocalDao.queryBuilder(); - userLocalQuery.selectColumns(UserLocal.COLUMN_NAME_ID); - Where where = userLocalQuery.where(); - where.eq(UserLocal.COLUMN_NAME_CURRENT_USER, Boolean.TRUE); - - QueryBuilder userQuery = userDao.queryBuilder(); - userQuery.where().in(User.COLUMN_NAME_USER_LOCAL_ID, userLocalQuery); - - PreparedQuery preparedQuery = userQuery.prepare(); - user = userDao.queryForFirst(preparedQuery); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem reading active users."); - throw new UserException("There was a problem reading active users.", sqle); - } - - return user; - } - - public boolean isCurrentUserPartOfCurrentEvent() { - try { - User user = readCurrentUser(); - Event currentEvent = user.getCurrentEvent(); - TeamHelper teamHelper = TeamHelper.getInstance(mApplicationContext); - List userTeams = teamHelper.getTeamsByUser(user); - Set eventTeams = new HashSet<>(teamHelper.getTeamsByEvent(currentEvent)); - eventTeams.retainAll(userTeams); - return eventTeams.size() > 0; - } - catch (Exception e) { - Log.e(LOG_NAME, "error determining current user event membership", e); - } - return false; - } - - @Override - public User update(User user) throws UserException { - try { - User oldUser = read(user.getId()); - user.setUserLocal(oldUser.getUserLocal()); - userDao.update(user); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating user: " + user); - throw new UserException("There was a problem creating user: " + user, sqle); - } - - for (IUserEventListener listener : userListeners) { - listener.onUserUpdated(user); - } - - return user; - } - - public User createOrUpdate(User user) { - try { - QueryBuilder db = userDao.queryBuilder(); - db.where().eq(User.COLUMN_NAME_USERNAME, user.getUsername()); - User oldUser = db.queryForFirst(); - - if (oldUser == null) { - user = create(user); - Log.d(LOG_NAME, "Created user with remote_id " + user.getRemoteId()); - } else { - // perform update? - user.setId(oldUser.getId()); - user.setUserLocal(oldUser.getUserLocal()); - userDao.update(user); - Log.d(LOG_NAME, "Updated user with remote_id " + user.getRemoteId()); - - for (IUserEventListener listener : userListeners) { - listener.onUserUpdated(user); - } - } - } catch (Exception ue) { - Log.e(LOG_NAME, "There was a problem reading user: " + user, ue); - } - return user; - } - - public User setCurrentUser(User user) throws UserException { - try { - clearCurrentUser(); - - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.where().idEq(user.getUserLocal().getId()); - builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_USER, true); - builder.update(); - - userDao.refresh(user); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to update user '" + user.getDisplayName() + "' to current user" , sqle); - throw new UserException("Unable to update UserLocal table", sqle); - } - - return user; - } - - public User setCurrentEvent(User user, Event event) throws UserException { - try { - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.where().idEq(user.getUserLocal().getId()); - builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_EVENT, event); - - // check if we need to send event onChange - UserLocal userLocal = user.getUserLocal(); - if (userLocal.isCurrentUser()) { - String oldEventRemoteId = null; - if (userLocal.getCurrentEvent() != null) { - oldEventRemoteId = userLocal.getCurrentEvent().getRemoteId(); - } - - String newEventRemoteId = event != null ? event.getRemoteId() : null; - - // run update before firing event to make sure update works. - builder.update(); - - if (oldEventRemoteId == null ^ newEventRemoteId == null) { - for (IEventEventListener listener : eventListeners) { - listener.onEventChanged(); - } - } else if (oldEventRemoteId != null && newEventRemoteId != null) { - if (!oldEventRemoteId.equals(newEventRemoteId)) { - for (IEventEventListener listener : eventListeners) { - listener.onEventChanged(); - } - } - } - - userDao.refresh(user); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to update users '" + user.getDisplayName() + "' current event" , sqle); - throw new UserException("Unable to update UserLocal table", sqle); - } - - return user; - } - - public User removeCurrentEvent(User user) throws UserException { - if (user == null || user.getUserLocal() == null) { - return user; - } - - try { - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.where().idEq(user.getUserLocal().getId()); - builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_EVENT, null); - builder.update(); - - userDao.refresh(user); - } catch (SQLException e) { - Log.e(LOG_NAME, "Unable to clear current event for user '" + user.getDisplayName() + "'"); - throw new UserException("Unable to update UserLocal table", e); - } - - return user; - } - - public User setAvatarPath(User user, String path) throws UserException { - try { - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.where().idEq(user.getUserLocal().getId()); - builder.updateColumnValue(UserLocal.COLUMN_NAME_AVATAR_PATH, path); - builder.update(); - - userLocalDao.refresh(user.getUserLocal()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to update users '" + user.getDisplayName() + "' avatar path" , sqle); - throw new UserException("Unable to update UserLocal table", sqle); - } - - for (IUserEventListener listener : userListeners) { - listener.onUserAvatarUpdated(user); - } - - return user; - } - - public User setIconPath(User user, String path) throws UserException { - try { - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.where().idEq(user.getUserLocal().getId()); - builder.updateColumnValue(UserLocal.COLUMN_NAME_ICON_PATH, path); - builder.update(); - - userLocalDao.refresh(user.getUserLocal()); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Unable to update users '" + user.getDisplayName() + "' icon path" , sqle); - throw new UserException("Unable to update UserLocal table", sqle); - } - - for (IUserEventListener listener : userListeners) { - listener.onUserIconUpdated(user); - } - - return user; - } - - private void clearCurrentUser() throws UserException { - try { - UpdateBuilder builder = userLocalDao.updateBuilder(); - builder.updateColumnValue(UserLocal.COLUMN_NAME_CURRENT_USER, Boolean.FALSE); - builder.update(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem deleting active userlocal.", sqle); - throw new UserException("There was a problem deleting active userlocal.", sqle); - } - } - - public void deleteUserTeams() { - try { - DeleteBuilder db = userTeamDao.deleteBuilder(); - db.delete(); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem deleting userteams.", sqle); - } - } - - public UserTeam create(UserTeam userTeam) { - UserTeam createdUserTeam = null; - try { - createdUserTeam = userTeamDao.createIfNotExists(userTeam); - } catch (SQLException sqle) { - Log.e(LOG_NAME, "There was a problem creating userteam: " + userTeam, sqle); - } - return createdUserTeam; - } - - public Collection getUsersInEvent(Event event) { - Collection users = new ArrayList<>(); - try { - QueryBuilder teamEventQuery = teamEventDao.queryBuilder(); - teamEventQuery.selectColumns("team_id"); - Where teamEventWhere = teamEventQuery.where(); - teamEventWhere.eq("event_id", event.getId()); - - QueryBuilder userTeamQuery = userTeamDao.queryBuilder(); - userTeamQuery.selectColumns("user_id"); - Where userTeamWhere = userTeamQuery.where(); - userTeamWhere.in("team_id", teamEventQuery); - - QueryBuilder teamQuery = userDao.queryBuilder(); - teamQuery.where().in("_id", userTeamQuery); - - users = teamQuery.query(); - if (users == null) { - users = new ArrayList<>(); - } - } catch (SQLException sqle) { - Log.e(LOG_NAME, "Error getting users for event: " + event, sqle); - } - - return users; - } - - @Override - public boolean addListener(IEventEventListener listener) { - return eventListeners.add(listener); - } - - @Override - public boolean removeListener(IEventEventListener listener) { - return eventListeners.remove(listener); - } - - @Override - public boolean addListener(IUserEventListener listener) { - return userListeners.add(listener); - } - - @Override - public boolean removeListener(IUserEventListener listener) { - return userListeners.add(listener); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IAttachmentEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IAttachmentEventListener.java index 23ef99bc9..da139f70a 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IAttachmentEventListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IAttachmentEventListener.java @@ -1,6 +1,6 @@ package mil.nga.giat.mage.sdk.event; -import mil.nga.giat.mage.sdk.datastore.observation.Attachment; +import mil.nga.giat.mage.database.model.observation.Attachment; public interface IAttachmentEventListener extends IEventListener { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILayerEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILayerEventListener.java deleted file mode 100644 index bbd0701e5..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILayerEventListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package mil.nga.giat.mage.sdk.event; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; - -public interface ILayerEventListener extends IEventListener { - - void onLayerCreated(Layer layer); - void onLayerUpdated(Layer layer); - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILocationEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILocationEventListener.java index 2bdcc2330..6db0ef1d1 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILocationEventListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/event/ILocationEventListener.java @@ -2,7 +2,7 @@ import java.util.Collection; -import mil.nga.giat.mage.sdk.datastore.location.Location; +import mil.nga.giat.mage.database.model.location.Location; public interface ILocationEventListener extends IEventListener { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IObservationEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IObservationEventListener.java index 5d39fd6e0..b37558fa8 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IObservationEventListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IObservationEventListener.java @@ -2,7 +2,7 @@ import java.util.Collection; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; +import mil.nga.giat.mage.database.model.observation.Observation; public interface IObservationEventListener extends IEventListener { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ISessionEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/ISessionEventListener.java deleted file mode 100644 index d98d97da8..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/ISessionEventListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package mil.nga.giat.mage.sdk.event; - - -public interface ISessionEventListener extends IEventListener { - - void onTokenExpired(); - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IStaticFeatureEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IStaticFeatureEventListener.java index d6484c82a..400a45ef7 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IStaticFeatureEventListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IStaticFeatureEventListener.java @@ -1,6 +1,6 @@ package mil.nga.giat.mage.sdk.event; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; +import mil.nga.giat.mage.database.model.layer.Layer; public interface IStaticFeatureEventListener extends IEventListener { diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IUserEventListener.java b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IUserEventListener.java index 211653af6..b577357cd 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/event/IUserEventListener.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/event/IUserEventListener.java @@ -1,7 +1,7 @@ package mil.nga.giat.mage.sdk.event; -import mil.nga.giat.mage.sdk.datastore.user.User; +import mil.nga.giat.mage.database.model.user.User; public interface IUserEventListener extends IEventListener { void onUserCreated(User user); diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/AbstractServerFetch.java b/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/AbstractServerFetch.java deleted file mode 100644 index c1a131970..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/AbstractServerFetch.java +++ /dev/null @@ -1,12 +0,0 @@ -package mil.nga.giat.mage.sdk.fetch; - -import android.content.Context; - -public abstract class AbstractServerFetch { - - protected Context mContext; - - public AbstractServerFetch(Context context) { - mContext = context; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/ImageryServerFetch.java b/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/ImageryServerFetch.java deleted file mode 100644 index adb745eae..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/ImageryServerFetch.java +++ /dev/null @@ -1,90 +0,0 @@ -package mil.nga.giat.mage.sdk.fetch; - -import android.content.Context; -import android.util.Log; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; - -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.http.resource.LayerResource; - -public class ImageryServerFetch extends AbstractServerFetch { - - private static final String LOG_NAME = ImageryServerFetch.class.getName(); - private static final String TYPE = "Imagery"; - - private final AtomicBoolean isCanceled = new AtomicBoolean(false); - - private final LayerResource layerResource; - - public ImageryServerFetch(Context context) { - super(context); - layerResource = new LayerResource(context); - } - - public void fetch() { - Event event = EventHelper.getInstance(mContext).getCurrentEvent(); - LayerHelper layerHelper = LayerHelper.getInstance(mContext); - - // if you are disconnect, skip this - if (!ConnectivityUtility.isOnline(mContext)) { - Log.d(LOG_NAME, "Disconnected, not pulling imagery."); - return; - } - - try { - Collection remoteLayers = layerResource.getImageryLayers(event); - - // get local layers - Collection localLayers = layerHelper.readAll(TYPE); - - Map remoteIdToLayer = new HashMap<>(localLayers.size()); - Iterator it = localLayers.iterator(); - while (it.hasNext()) { - Layer localLayer = it.next(); - - //See if the layer has been deleted on the server - if (!remoteLayers.contains(localLayer)){ - it.remove(); - layerHelper.delete(localLayer.getId()); - } else { - remoteIdToLayer.put(localLayer.getRemoteId(), localLayer); - } - } - - - for (Layer remoteLayer : remoteLayers) { - if (isCanceled.get()) { - break; - } - remoteLayer.setEvent(event); - remoteLayer.setLoaded(true); - - if (!localLayers.contains(remoteLayer)) { - //New layer from remote server - layerHelper.create(remoteLayer); - } else { - Layer localLayer = remoteIdToLayer.get(remoteLayer.getRemoteId()); - layerHelper.delete(localLayer.getId()); - layerHelper.create(remoteLayer); - } - } - } catch(Exception e) { - Log.w(LOG_NAME, "Error performing imagery layer operations",e); - } - } - - - public void destroy() { - isCanceled.getAndSet(true); - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/StaticFeatureServerFetch.java b/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/StaticFeatureServerFetch.java deleted file mode 100644 index a31cb09c4..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/StaticFeatureServerFetch.java +++ /dev/null @@ -1,211 +0,0 @@ -package mil.nga.giat.mage.sdk.fetch; - -import android.content.Context; -import android.util.Log; - -import com.google.common.io.ByteStreams; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.layer.LayerHelper; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureHelper; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureProperty; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.exceptions.StaticFeatureException; -import mil.nga.giat.mage.sdk.http.resource.LayerResource; - -public class StaticFeatureServerFetch extends AbstractServerFetch { - - public interface OnStaticLayersListener { - void onStaticLayersLoaded(Collection layers); - } - - private final LayerResource layerResource; - - public StaticFeatureServerFetch(Context context) { - super(context); - layerResource = new LayerResource(context); - } - - private static final String LOG_NAME = StaticFeatureServerFetch.class.getName(); - private static final String FEATURE_TYPE = "Feature"; - - private Boolean isCanceled = Boolean.FALSE; - - - // TODO test that icons are pulled correctly - public List fetch(boolean deleteLocal, OnStaticLayersListener listener) { - - List newLayers = new ArrayList<>(); - - // if you are disconnect, skip this - if (!ConnectivityUtility.isOnline(mContext)) { - Log.d(LOG_NAME, "Disconnected, not pulling static layers."); - return newLayers; - } - - Event event = EventHelper.getInstance(mContext).getCurrentEvent(); - Log.d(LOG_NAME, "Pulling static layers for event " + event.getName()); - try { - LayerHelper layerHelper = LayerHelper.getInstance(mContext); - - if (deleteLocal) { - layerHelper.deleteAll(FEATURE_TYPE); - } - - Collection remoteLayers = layerResource.getLayers(event); - - // get local layers - Collection localLayers = layerHelper.readAll(FEATURE_TYPE); - - Map remoteIdToLayer = new HashMap<>(localLayers.size()); - Iterator it = localLayers.iterator(); - while(it.hasNext()) { - Layer localLayer = it.next(); - - //See if the layer has been deleted on the server - if(!remoteLayers.contains(localLayer)){ - it.remove(); - layerHelper.delete(localLayer.getId()); - }else{ - remoteIdToLayer.put(localLayer.getRemoteId(), localLayer); - } - } - - for (Layer remoteLayer : remoteLayers) { - remoteLayer.setEvent(event); - if (!localLayers.contains(remoteLayer)) { - layerHelper.create(remoteLayer); - } else { - Layer localLayer = remoteIdToLayer.get(remoteLayer.getRemoteId()); - if(!remoteLayer.getEvent().equals(localLayer.getEvent())) { - layerHelper.delete(localLayer.getId()); - layerHelper.create(remoteLayer); - } - } - } - - newLayers.addAll(layerHelper.readAll(FEATURE_TYPE)); - - if (listener != null) { - listener.onStaticLayersLoaded(newLayers); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Problem creating layers.", e); - } - - return newLayers; - } - - public void load(OnStaticLayersListener listener, Layer layer) { - // if you are disconnect, skip this - if (!ConnectivityUtility.isOnline(mContext)) { - Log.d(LOG_NAME, "Disconnected, not loading static features."); - return; - } - - try { - if (!layer.isLoaded()) { - StaticFeatureHelper staticFeatureHelper = StaticFeatureHelper.getInstance(mContext); - try { - try { - layer.setDownloadId(1l); - LayerHelper.getInstance(mContext).update(layer); - } catch (Exception e) { - throw new StaticFeatureException("Unable to update the layer to loaded: " + layer.getName()); - } - Log.i(LOG_NAME, "Loading static features for layer " + layer.getName() + "."); - - Collection staticFeatures = layerResource.getFeatures(layer); - - // Pull down the icons - Collection failedIconUrls = new ArrayList<>(); - for (StaticFeature staticFeature : staticFeatures) { - StaticFeatureProperty property = staticFeature.getPropertiesMap().get("styleiconstyleiconhref"); - if (property != null) { - String iconUrlString = property.getValue(); - - if (failedIconUrls.contains(iconUrlString)) { - continue; - } - - if (iconUrlString != null) { - File iconFile = null; - try { - URL iconUrl = new URL(iconUrlString); - String filename = iconUrl.getFile(); - // remove leading / - if (filename != null) { - filename = filename.trim(); - while (filename.startsWith("/")) { - filename = filename.substring(1); - } - } - - iconFile = new File(mContext.getFilesDir() + "/icons/staticfeatures", filename); - if (!iconFile.exists()) { - iconFile.getParentFile().mkdirs(); - iconFile.createNewFile(); - InputStream inputStream = layerResource.getFeatureIcon(iconUrlString); - if (inputStream != null) { - ByteStreams.copy(inputStream, new FileOutputStream(iconFile)); - staticFeature.setLocalPath(iconFile.getAbsolutePath()); - } - } else { - staticFeature.setLocalPath(iconFile.getAbsolutePath()); - } - } catch (Exception e) { - // this block should never flow exceptions up! Log for now. - Log.w(LOG_NAME, "Could not get icon.", e); - failedIconUrls.add(iconUrlString); - if (iconFile != null && iconFile.exists()) { - iconFile.delete(); - } - } - } - } - } - - layer = staticFeatureHelper.createAll(staticFeatures, layer); - try { - layer.setLoaded(true); - layer.setDownloadId(null); - LayerHelper.getInstance(mContext).update(layer); - } catch (Exception e) { - throw new StaticFeatureException("Unable to update the layer to loaded: " + layer.getName()); - } - - Log.i(LOG_NAME, "Loaded static features for layer " + layer.getName()); - - if(listener != null){ - List layers = new ArrayList<>(1); - layers.add(layer); - listener.onStaticLayersLoaded(layers); - } - - } catch (StaticFeatureException e) { - Log.e(LOG_NAME, "Problem creating static features.", e); - } - } - } catch (Exception e) { - Log.e(LOG_NAME, "Problem loading layers.", e); - } - } - - public void destroy() { - isCanceled = true; - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/UserServerFetch.java b/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/UserServerFetch.java deleted file mode 100644 index 40bae501d..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/fetch/UserServerFetch.java +++ /dev/null @@ -1,42 +0,0 @@ -package mil.nga.giat.mage.sdk.fetch; - -import android.content.Context; -import android.util.Log; - -import java.util.Date; - -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.http.resource.UserResource; - -public class UserServerFetch extends AbstractServerFetch { - - public UserServerFetch(Context context) { - super(context); - } - - private static final String LOG_NAME = UserServerFetch.class.getName(); - - public void fetch(String... userIds) throws Exception { - - try { - UserResource userResource = new UserResource(mContext); - UserHelper userHelper = UserHelper.getInstance(mContext); - - // loop over all the ids - for (String userId : userIds) { - if (userId.equals("-1")) { - continue; - } - - User user = userResource.getUser(userId); - if (user != null) { - user.setFetchedDate(new Date()); - userHelper.createOrUpdate(user); - } - } - } catch(Exception e) { - Log.e(LOG_NAME, "Problem fetching users.", e); - } - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventsDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventsDeserializer.java deleted file mode 100644 index 5162a5a71..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/EventsDeserializer.java +++ /dev/null @@ -1,96 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; - -import android.content.Context; -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.Team; -import mil.nga.giat.mage.sdk.datastore.user.TeamHelper; -import mil.nga.giat.mage.sdk.exceptions.TeamException; - -/** - * JSON to {@link Event} - * - * @author newmanw - * - */ -public class EventsDeserializer implements JsonDeserializer>> { - - private static final String LOG_NAME = EventsDeserializer.class.getName(); - - private final TeamHelper teamHelper; - private final Gson teamDeserializer; - private final Gson eventDeserializer; - - public EventsDeserializer(Context context) { - teamHelper = TeamHelper.getInstance(context); - teamDeserializer = TeamDeserializer.getGsonBuilder(); - eventDeserializer = EventDeserializer.getGsonBuilder(); - } - - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Event}. - */ - public static Gson getGsonBuilder(Context context) { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(new TypeToken>>(){}.getType(), new EventsDeserializer(context)); - return gsonBuilder.create(); - } - - @Override - public Map> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - Map> events = new HashMap<>(); - - for (JsonElement element : json.getAsJsonArray()) { - JsonObject jsonEvent = element.getAsJsonObject(); - Event event = eventDeserializer.fromJson(jsonEvent, Event.class); - Collection teams = deserializeTeams(jsonEvent.getAsJsonArray("teams")); - events.put(event, teams); - } - - return events; - } - - private Collection deserializeTeams(JsonArray jsonTeams) { - Collection teams = new ArrayList<>(); - for (JsonElement element : jsonTeams) { - JsonObject jsonTeam = element.getAsJsonObject(); - - Team team = null; - try { - team = teamHelper.read(jsonTeam.get("id").getAsString()); - } catch (TeamException e) { - Log.e(LOG_NAME, "Error reading user from database", e); - } - - if (team == null) { - team = teamDeserializer.fromJson(jsonTeam.toString(), Team.class); - } - - if (team != null) { - teams.add(team); - } - } - - return teams; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RoleDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RoleDeserializer.java deleted file mode 100644 index 2a01409d9..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RoleDeserializer.java +++ /dev/null @@ -1,69 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; - -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Locale; - -import mil.nga.giat.mage.sdk.datastore.user.Permission; -import mil.nga.giat.mage.sdk.datastore.user.Permissions; -import mil.nga.giat.mage.sdk.datastore.user.Role; - -/** - * JSON to {@link Role} - * - * @author wiedemanns - * - */ -public class RoleDeserializer implements JsonDeserializer { - - private static final String LOG_NAME = RoleDeserializer.class.getName(); - - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Role}. - */ - public static Gson getGsonBuilder() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Role.class, new RoleDeserializer()); - return gsonBuilder.create(); - } - - @Override - public Role deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonRole = json.getAsJsonObject(); - - String remoteId = jsonRole.get("id").getAsString(); - String name = jsonRole.get("name").getAsString(); - String description = jsonRole.get("description").getAsString(); - - - Collection permissions = new ArrayList<>(); - for (JsonElement element : jsonRole.get("permissions").getAsJsonArray()) { - String jsonPermission = element.getAsString(); - if (jsonPermission != null) { - jsonPermission = jsonPermission.toUpperCase(Locale.US); - try { - Permission permission = Permission.valueOf(jsonPermission); - permissions.add(permission); - } catch (IllegalArgumentException iae) { - Log.e(LOG_NAME, "Could not find matching permission, " + jsonPermission + ", for user."); - } - } - } - - return new Role(remoteId, name, description, new Permissions(permissions)); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RolesDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RolesDeserializer.java deleted file mode 100644 index 5f8b7b7f6..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/RolesDeserializer.java +++ /dev/null @@ -1,58 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; - -import mil.nga.giat.mage.sdk.datastore.user.Role; - -/** - * JSON to {@link Role} - * - * @author newmanw - * - */ -public class RolesDeserializer implements JsonDeserializer> { - - private static final String LOG_NAME = RolesDeserializer.class.getName(); - - private final Gson roleDeserializer; - - public RolesDeserializer() { - roleDeserializer = RoleDeserializer.getGsonBuilder(); - } - - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Role}. - */ - public static Gson getGsonBuilder() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(new TypeToken>(){}.getType(), new RolesDeserializer()); - return gsonBuilder.create(); - } - - @Override - public Collection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - Collection roles = new ArrayList<>(); - - JsonArray jsonRoles = json.getAsJsonArray(); - for (JsonElement element : jsonRoles) { - Role role = roleDeserializer.fromJson(element, Role.class); - roles.add(role); - } - - return roles; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamDeserializer.java deleted file mode 100644 index f47dc541a..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamDeserializer.java +++ /dev/null @@ -1,52 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -import java.lang.reflect.Type; - -import mil.nga.giat.mage.sdk.datastore.user.Team; - -/** - * JSON to {@link Team} - * - * @author wiedemanns - * - */ -public class TeamDeserializer implements JsonDeserializer { - - private static final String LOG_NAME = TeamDeserializer.class.getName(); - - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Team}. - */ - public static Gson getGsonBuilder() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(Team.class, new TeamDeserializer()); - return gsonBuilder.create(); - } - - @Override - public Team deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - JsonObject jsonTeam = json.getAsJsonObject(); - - String remoteId = jsonTeam.get("id").getAsString(); - String name = jsonTeam.get("name").getAsString(); - - String description = ""; - if (jsonTeam.has("description")) { - description = jsonTeam.get("description").toString(); - } - - return new Team(remoteId, name, description); - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamsDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamsDeserializer.java deleted file mode 100644 index 06d09d7dd..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/deserializer/TeamsDeserializer.java +++ /dev/null @@ -1,89 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.deserializer; - -import android.content.Context; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; - -import java.io.StringReader; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import mil.nga.giat.mage.network.gson.user.UserTypeAdapter; -import mil.nga.giat.mage.sdk.datastore.user.Team; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; - -/** - * JSON to {@link Team} - */ -public class TeamsDeserializer implements JsonDeserializer>> { - - private final UserHelper userHelper; - private final UserTypeAdapter userTypeAdapter; - private final Gson teamDeserializer; - - public TeamsDeserializer(Context context) { - userHelper = UserHelper.getInstance(context); - userTypeAdapter = new UserTypeAdapter(context); - teamDeserializer = TeamDeserializer.getGsonBuilder(); - } - - /** - * Convenience method for returning a Gson object with a registered GSon - * TypeAdaptor i.e. custom deserializer. - * - * @return A Gson object that can be used to convert Json into a {@link Team}. - */ - public static Gson getGsonBuilder(Context context) { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(new TypeToken>>(){}.getType(), new TeamsDeserializer(context)); - return gsonBuilder.create(); - } - - @Override - public Map> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - Map> teams = new HashMap<>(); - - for (JsonElement element : json.getAsJsonArray()) { - JsonObject jsonTeam = element.getAsJsonObject(); - Team team = teamDeserializer.fromJson(jsonTeam, Team.class); - Collection users = deserializeUsers(jsonTeam.getAsJsonArray("users")); - teams.put(team, users); - } - - return teams; - } - - private Collection deserializeUsers(JsonArray jsonUsers) { - Collection users = new ArrayList<>(); - for (JsonElement userElement : jsonUsers) { - JsonObject jsonUser = userElement.getAsJsonObject(); - - try { - JsonReader reader = new JsonReader(new StringReader(jsonUser.toString())); - User user = userTypeAdapter.read(reader); - User existingUser = userHelper.read(user.getRemoteId()); - if (existingUser != null) { - user.setId(existingUser.getId()); - } - users.add(user); - } catch (Exception e) { - e.printStackTrace(); - } - } - - return users; - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/LocationSerializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/LocationSerializer.java deleted file mode 100644 index c427aeb58..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/gson/serializer/LocationSerializer.java +++ /dev/null @@ -1,98 +0,0 @@ -package mil.nga.giat.mage.sdk.gson.serializer; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; -import com.google.gson.reflect.TypeToken; - -import java.io.Serializable; -import java.lang.reflect.Type; -import java.text.DateFormat; -import java.util.Collection; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; -import mil.nga.giat.mage.sdk.datastore.observation.Observation; -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory; - -/** - * Used to convert a Location object into a json String. - * - * @author travis - * - */ -public class LocationSerializer implements JsonSerializer> { - - @Override - public JsonElement serialize(Collection locations, Type locationType, JsonSerializationContext context) { - // create required components - JsonArray jsonLocations = new JsonArray(); - for (Location location : locations) { - - JsonObject jsonLocation = new JsonObject(); - jsonLocation.add("eventId", new JsonPrimitive(location.getEvent().getRemoteId())); - JsonObject jsonProperties = new JsonObject(); - - jsonLocation.add("geometry", new JsonParser().parse(GeometrySerializer.getGsonBuilder().toJson(location.getGeometry()))); - jsonLocation.add("properties", jsonProperties); - jsonProperties.add("timestamp", new JsonPrimitive(ISO8601DateFormatFactory.ISO8601().format(location.getTimestamp()))); - - // properties - for (LocationProperty property : location.getProperties()) { - String key = property.getKey(); - Serializable value = property.getValue(); - - conditionalAdd(key, value, jsonProperties); - } - // assemble final location array - jsonLocations.add(jsonLocation); - } - - return jsonLocations; - } - - /** - * Convenience method for returning a Gson object with a registered GSon TypeAdaptor i.e. custom serializer. - * - * @return A Gson object that can be used to convert {@link Observation} object into a JSON string. - */ - public static Gson getGsonBuilder() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(new TypeToken>(){}.getType(), new LocationSerializer()); - return gsonBuilder.create(); - } - - /** - * Utility used to ensure we don't add junk to the json string. For now, we skip null property values. - * - * @param property - * Property to add. - * @param toAdd - * Property value to add. - * @param pJsonObject - * Object to conditionally add to. - * @return A reference to json object. - */ - private JsonObject conditionalAdd(String property, Serializable toAdd, final JsonObject pJsonObject) { - if (toAdd != null) { - if (toAdd instanceof Double) { - pJsonObject.add(property, new JsonPrimitive((Double) toAdd)); - } else if (toAdd instanceof Float) { - pJsonObject.add(property, new JsonPrimitive((Float) toAdd)); - } else if (toAdd instanceof Boolean) { - pJsonObject.add(property, new JsonPrimitive((Boolean) toAdd)); - } else { - pJsonObject.add(property, new JsonPrimitive(toAdd.toString())); - } - } - return pJsonObject; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/HttpClientManager.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/HttpClientManager.java deleted file mode 100644 index 90dfe7be9..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/HttpClientManager.java +++ /dev/null @@ -1,136 +0,0 @@ -package mil.nga.giat.mage.sdk.http; - -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; - -import android.app.Application; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.TimeUnit; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.network.Server; -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.ISessionEventListener; -import mil.nga.giat.mage.sdk.utils.UserUtility; -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - -/** - * Always use the {@link HttpClientManager#httpClient} for making ALL - * requests to the server. This class adds request and response interceptors to - * pass things like a token and handle errors like 403 and 401. - * - * @author newmanw - */ -public class HttpClientManager implements IEventDispatcher { - - private static final String LOG_NAME = HttpClientManager.class.getName(); - - private static HttpClientManager instance; - - private final Application context; - private final String userAgent; - private OkHttpClient client; - private Server server; - - private final Collection listeners = new CopyOnWriteArrayList<>(); - - public static synchronized HttpClientManager initialize(Application context, Server server) { - if (instance != null) { - throw new Error("attempt to initialize " + HttpClientManager.class.getName() + " singleton more than once"); - } - - String userAgent = System.getProperty("http.agent"); - userAgent = userAgent == null ? "" : userAgent; - - instance = new HttpClientManager(context, userAgent, server); - - return instance; - } - - public static HttpClientManager getInstance() { - return instance; - } - - private HttpClientManager(Application context, String userAgent, Server server) { - this.context = context; - this.userAgent = userAgent; - this.server = server; - - initializeClient(); - } - - private void initializeClient() { - OkHttpClient.Builder builder = new OkHttpClient.Builder() - .connectTimeout(60, TimeUnit.SECONDS) - .readTimeout(60, TimeUnit.SECONDS) - .writeTimeout(60, TimeUnit.SECONDS); - - builder.addInterceptor(chain -> { - Request.Builder builder1 = chain.request().newBuilder(); - - // add token - String token = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.tokenKey), null); - if (token != null && !token.trim().isEmpty()) { - builder1.addHeader("Authorization", "Bearer " + token); - } - - builder1.addHeader("User-Agent", userAgent); - - Response response = chain.proceed(builder1.build()); - - int statusCode = response.code(); - if (statusCode == HTTP_UNAUTHORIZED) { - // If token has not expired yet, expire it and send notification to listeners - if (hasToken()) { - UserUtility.getInstance(context).clearTokenInformation(); - - for (ISessionEventListener listener : listeners) { - listener.onTokenExpired(); - } - } - - Log.w(LOG_NAME, "TOKEN EXPIRED"); - } else if (statusCode == HTTP_NOT_FOUND) { - Log.w(LOG_NAME, "404 Not Found."); - } - - return response; - }); - - client = builder.build(); - } - - public OkHttpClient httpClient() { - return client; - } - - @Override - public boolean addListener(ISessionEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(ISessionEventListener listener) { - return listeners.remove(listener); - } - - private Boolean hasToken() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - String token = preferences.getString(context.getString(R.string.tokenKey), ""); - String tokenExpiration = preferences.getString(context.getString(R.string.tokenExpirationDateKey), ""); - - return !token.isEmpty() || !tokenExpiration.isEmpty(); - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureConverterFactory.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureConverterFactory.java deleted file mode 100644 index accb68a56..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureConverterFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -package mil.nga.giat.mage.sdk.http.converter; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import okhttp3.ResponseBody; -import retrofit2.Converter; -import retrofit2.Retrofit; - -/** - * Retrofit converter factory for features - * - * @author newmanw - * - */ -public final class FeatureConverterFactory extends Converter.Factory { - - private final Layer layer; - - public static FeatureConverterFactory create(Layer layer) { - return new FeatureConverterFactory(layer); - } - - private FeatureConverterFactory(Layer layer) { - this.layer = layer; - } - - @Override - public Converter> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { - return new FeatureResponseBodyConverter(layer); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureResponseBodyConverter.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureResponseBodyConverter.java deleted file mode 100644 index ccf67973d..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/FeatureResponseBodyConverter.java +++ /dev/null @@ -1,33 +0,0 @@ -package mil.nga.giat.mage.sdk.http.converter; - -import java.io.IOException; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.jackson.deserializer.StaticFeatureDeserializer; -import okhttp3.ResponseBody; -import retrofit2.Converter; - -/** - * Retrofit feature response body converter - * - * Handles Jackson deserialization for features - * - * @author newmanw - * - */ -public class FeatureResponseBodyConverter implements Converter> { - - private final Layer layer; - - public FeatureResponseBodyConverter(Layer layer) { - this.layer = layer; - } - - @Override - public List convert(ResponseBody body) throws IOException { - return new StaticFeatureDeserializer().parseStaticFeatures(body.byteStream(), layer); - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationConverterFactory.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationConverterFactory.java deleted file mode 100644 index b8bacca56..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationConverterFactory.java +++ /dev/null @@ -1,50 +0,0 @@ -package mil.nga.giat.mage.sdk.http.converter; - -import com.google.gson.Gson; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.gson.serializer.LocationSerializer; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Converter; -import retrofit2.Retrofit; - -/** - * Retrofit converter factory for locations - * - * @author newmanw - * - */ -public final class LocationConverterFactory extends Converter.Factory { - - private final Gson gson; - private final Event event; - private final boolean groupByUser; - - public static LocationConverterFactory create(Event event, boolean groupByUser) { - return new LocationConverterFactory(LocationSerializer.getGsonBuilder(), event, groupByUser); - } - - private LocationConverterFactory(Gson gson, Event event, boolean groupByUser) { - this.gson = gson; - this.event = event; - this.groupByUser = groupByUser; - } - - - @Override - public Converter> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) { - return new LocationResponseBodyConverter(event, groupByUser); - } - - @Override - public Converter, RequestBody> requestBodyConverter(Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) { - return new LocationRequestBodyConverter(gson, type); - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationRequestBodyConverter.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationRequestBodyConverter.java deleted file mode 100644 index fe583b22c..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationRequestBodyConverter.java +++ /dev/null @@ -1,52 +0,0 @@ -package mil.nga.giat.mage.sdk.http.converter; - -import com.google.gson.Gson; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.lang.reflect.Type; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.location.Location; -import okhttp3.MediaType; -import okhttp3.RequestBody; -import okio.Buffer; -import retrofit2.Converter; - -/** - * Retrofit location request body converter - * - * Handles GSON serialization for locations - * - * @author newmanw - * - */ -public class LocationRequestBodyConverter implements Converter, RequestBody> { - - private static final MediaType MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8"); - private static final Charset UTF_8 = StandardCharsets.UTF_8; - - private final Gson gson; - private final Type type; - - LocationRequestBodyConverter(Gson gson, Type type) { - this.gson = gson; - this.type = type; - } - - @Override - public RequestBody convert(List value) throws IOException { - Buffer buffer = new Buffer(); - Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8); - try { - gson.toJson(value, type, writer); - writer.flush(); - } catch (IOException e) { - throw new AssertionError(e); // Writing to Buffer does no I/O. - } - return RequestBody.create(MEDIA_TYPE, buffer.readByteString()); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationResponseBodyConverter.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationResponseBodyConverter.java deleted file mode 100644 index 792f17710..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/converter/LocationResponseBodyConverter.java +++ /dev/null @@ -1,37 +0,0 @@ -package mil.nga.giat.mage.sdk.http.converter; - -import java.io.IOException; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.jackson.deserializer.LocationDeserializer; -import okhttp3.ResponseBody; -import retrofit2.Converter; - -/** - * Retrofit location response body converter - * - * Handles Jackson deserialization for locations - * - * @author newmanw - * - */ -public class LocationResponseBodyConverter implements Converter> { - - private final Event event; - private final boolean groupByUser; - - public LocationResponseBodyConverter(Event event, boolean groupByUser) { - this.event = event; - this.groupByUser = groupByUser; - } - - @Override - public List convert(ResponseBody body) throws IOException { - LocationDeserializer deserializer = new LocationDeserializer(event); - return groupByUser ? - deserializer.parseUserLocations(body.byteStream()) : - deserializer.parseLocations(body.byteStream()); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ApiResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ApiResource.java deleted file mode 100644 index 978487555..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ApiResource.java +++ /dev/null @@ -1,47 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; - -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Retrofit; -import retrofit2.http.GET; - -/*** - * RESTful communication for the API - * - * @author newmanw - */ - -public class ApiResource { - - public interface ApiService { - - @GET("/api") - Call getApi(); - } - - private static final String LOG_NAME = ApiResource.class.getName(); - - private final Context context; - - public ApiResource(Context context) { - this.context = context; - } - - public void getApi(String url, Callback callback) { - try { - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(url) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - ApiService service = retrofit.create(ApiService.class); - service.getApi().enqueue(callback); - } catch (IllegalArgumentException e) { - callback.onFailure(null, e); - } - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/DeviceResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/DeviceResource.java deleted file mode 100644 index ab1f97140..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/DeviceResource.java +++ /dev/null @@ -1,78 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.gson.JsonObject; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import okhttp3.OkHttpClient; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.Body; -import retrofit2.http.Header; -import retrofit2.http.POST; - -/*** - * RESTful communication for devices - * - * @author newmanw - */ - -public class DeviceResource { - - public interface DeviceService { - @POST("/auth/token") - Call authorize(@Header("Authorization") String authorization, @Header("user-agent") String userAgent, @Body JsonObject body); - } - - private static final String LOG_NAME = DeviceResource.class.getName(); - - private final Context context; - - public DeviceResource(Context context) { - this.context = context; - } - - public Response authorize(String token, String uid) { - Response response = null; - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - try { - OkHttpClient.Builder builder = HttpClientManager.getInstance().httpClient().newBuilder(); - builder.interceptors().clear(); - OkHttpClient client = builder.build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(client) - .build(); - - DeviceService service = retrofit.create(DeviceService.class); - - JsonObject json = new JsonObject(); - json.addProperty("uid", uid); - - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - json.addProperty("appVersion", String.format("%s-%s", packageInfo.versionName, packageInfo.versionCode)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_NAME , "Problem retrieving package info.", e); - } - - response = service.authorize(String.format("Bearer %s", token), System.getProperty("http.agent"), json).execute(); - } catch (Exception e) { - Log.e(LOG_NAME, "Bad request.", e); - } - - return response; - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LayerResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LayerResource.java deleted file mode 100644 index 61a5962a1..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LayerResource.java +++ /dev/null @@ -1,155 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.gson.deserializer.LayerDeserializer; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import mil.nga.giat.mage.sdk.http.converter.FeatureConverterFactory; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.GET; -import retrofit2.http.Path; -import retrofit2.http.Query; -import retrofit2.http.Url; - -/*** - * RESTful communication for events - * - * @author newmanw - */ - -public class LayerResource { - - public interface LayerService { - - @GET("/api/events/{eventId}/layers") - Call> getLayers(@Path("eventId") String eventId, @Query("type") String type); - - @GET("/api/events/{eventId}/layers/{layerId}/features") - Call> getFeatures(@Path("eventId") String eventId, @Path("layerId") String layerId); - - @GET - Call getFeatureIcon(@Url String url); - } - - private static final String LOG_NAME = LayerResource.class.getName(); - - private final Context context; - - public LayerResource(Context context) { - this.context = context; - } - - public Collection getLayers(Event event) throws IOException { - Collection layers = new ArrayList<>(); - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(LayerDeserializer.getGsonBuilder())) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LayerService service = retrofit.create(LayerService.class); - Response> response = service.getLayers(event.getRemoteId(), "Feature").execute(); - - if (response.isSuccessful()) { - layers = response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return layers; - } - - public Collection getImageryLayers(Event event) throws IOException { - Collection layers = new ArrayList<>(); - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(LayerDeserializer.getGsonBuilder())) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LayerService service = retrofit.create(LayerService.class); - Response> response = service.getLayers(event.getRemoteId(), "Imagery").execute(); - - if (response.isSuccessful()) { - layers = response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return layers; - } - - public Collection getFeatures(Layer layer) throws IOException { - Collection features = new ArrayList<>(); - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(FeatureConverterFactory.create(layer)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LayerService service = retrofit.create(LayerService.class); - Response> response = service.getFeatures(layer.getEvent().getRemoteId(), layer.getRemoteId()).execute(); - - if (response.isSuccessful()) { - features = response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return features; - } - - public InputStream getFeatureIcon(String url) throws IOException { - InputStream inputStream = null; - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LayerService service = retrofit.create(LayerService.class); - Response response = service.getFeatureIcon(url).execute(); - - if (response.isSuccessful()) { - inputStream = response.body().byteStream(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return inputStream; - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LocationResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LocationResource.java deleted file mode 100644 index a88f4478b..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/LocationResource.java +++ /dev/null @@ -1,94 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.util.List; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.location.LocationHelper; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import mil.nga.giat.mage.sdk.http.converter.LocationConverterFactory; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.http.Body; -import retrofit2.http.POST; -import retrofit2.http.Path; - -/*** - * RESTful communication for locations - */ - -public class LocationResource { - - public interface LocationService { - @POST("/api/events/{eventId}/locations") - Call> createLocations(@Path("eventId") String eventId, @Body List locations); - } - - private static final String LOG_NAME = LocationResource.class.getName(); - - private final Context context; - - public LocationResource(Context context) { - this.context = context; - } - - /** - * All these locations provided should be from the provided event. - * - * @param locations locations to post to the server - * @param event event in which to post locations for - * @return create status - */ - public boolean createLocations(Event event, List locations) { - LocationHelper locationHelper = LocationHelper.getInstance(context); - - try { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(LocationConverterFactory.create(event, false)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - LocationService service = retrofit.create(LocationService.class); - Call> call = service.createLocations(event.getRemoteId(), locations); - Response> response = call.execute(); - - if (response.isSuccessful()) { - // locations that are posted are only from the current user - User user = UserHelper.getInstance(context).readCurrentUser(); - - // it is imperative that the order of the returnedLocations match the order that was posted!!! - // if the order changes from the server, all of this will break! - List returnedLocations = response.body(); - for (int i = 0; i < returnedLocations.size(); i++) { - Location returnedLocation = returnedLocations.get(i); - returnedLocation.setId(locations.get(i).getId()); - returnedLocation.setUser(user); - locationHelper.update(returnedLocation); - } - - return true; - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - } catch (Exception e) { - Log.e(LOG_NAME, "Failure posting location.", e); - } - - return false; - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ObservationResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ObservationResource.java deleted file mode 100644 index 1c58c8245..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/ObservationResource.java +++ /dev/null @@ -1,73 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.io.IOException; -import java.util.Map; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.datastore.observation.Attachment; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.http.GET; -import retrofit2.http.Multipart; -import retrofit2.http.PUT; -import retrofit2.http.PartMap; -import retrofit2.http.Path; -import retrofit2.http.Streaming; - -/*** - * RESTful communication for observations - */ -public class ObservationResource { - - public interface ObservationService { - @Streaming - @GET("/api/events/{eventId}/observations/{observationId}/attachments/{attachmentId}") - Call getAttachment(@Path("eventId") String eventId, @Path("observationId") String observationId, @Path("attachmentId") String attachmentId); - - @Multipart - @PUT("/api/events/{eventId}/observations/{observationId}/attachments/{attachmentId}") - Call createAttachment(@Path("eventId") String eventId, @Path("observationId") String observationId, @Path("attachmentId") String attachmentId, @PartMap Map parts); - } - - private static final String LOG_NAME = ObservationResource.class.getName(); - - private final Context context; - - public ObservationResource(Context context) { - this.context = context; - } - - public ResponseBody getAttachment(Attachment attachment) throws IOException { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - ObservationService service = retrofit.create(ObservationService.class); - - String eventId = attachment.getObservation().getEvent().getRemoteId(); - String observationId = attachment.getObservation().getRemoteId(); - String attachmentId = attachment.getRemoteId(); - Response response = service.getAttachment(eventId, observationId, attachmentId).execute(); - - if (response.isSuccessful()) { - return response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return null; - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/UserResource.java b/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/UserResource.java deleted file mode 100644 index 445582d39..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/http/resource/UserResource.java +++ /dev/null @@ -1,350 +0,0 @@ -package mil.nga.giat.mage.sdk.http.resource; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.network.gson.user.UserTypeAdapter; -import mil.nga.giat.mage.network.gson.user.UsersTypeAdapter; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.http.HttpClientManager; -import mil.nga.giat.mage.sdk.utils.MediaUtility; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.RequestBody; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import retrofit2.Retrofit; -import retrofit2.converter.gson.GsonConverterFactory; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.Header; -import retrofit2.http.Multipart; -import retrofit2.http.POST; -import retrofit2.http.PUT; -import retrofit2.http.PartMap; -import retrofit2.http.Path; - -/*** - * RESTful communication for users - */ - -public class UserResource { - - public interface UserService { - @POST("/auth/{strategy}/signin") - Call signin(@Path("strategy") String strategy, @Body JsonObject body); - - @POST("/api/logout") - Call logout(@Header("Authorization") String authorization); - - @GET("/api/users") - Call> getUsers(); - - @POST("/api/users/signups") - Call signup(@Body JsonObject body); - - @POST("/api/users/signups/verifications") - Call signupVerify(@Header("Authorization") String authorization, @Body JsonObject body); - - @GET("/api/users/{userId}") - Call getUser(@Path("userId") String userId); - - @GET("/api/users/{userId}/icon") - Call getIcon(@Path("userId") String userId); - - @GET("/api/users/{userId}/avatar") - Call getAvatar(@Path("userId") String userId); - - @POST("/api/users/{userId}/events/{eventId}/recent") - Call addRecentEvent(@Path("userId") String userId, @Path("eventId") String eventId); - - @Multipart - @PUT("/api/users/myself") - Call createAvatar(@PartMap Map parts); - - @PUT("/api/users/myself/password") - Call changePassword(@Body JsonObject body); - } - - private static final String LOG_NAME = UserResource.class.getName(); - - private final Context context; - - public UserResource(Context context) { - this.context = context; - } - - public Response signin(String strategy, JsonObject body) { - Response response = null; - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - try { - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - - try { - PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); - body.addProperty("appVersion", String.format("%s-%s", packageInfo.versionName, packageInfo.versionCode)); - } catch (PackageManager.NameNotFoundException e) { - Log.e(LOG_NAME , "Problem retrieving package info.", e); - } - - response = service.signin(strategy, body).execute(); - } catch (Exception e) { - Log.e(LOG_NAME, "Bad request.", e); - } - - return response; - } - - public void logout(Callback callback) { - try { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - - String token = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.tokenKey), null); - service.logout(String.format("Bearer %s", token)).enqueue(callback); - } catch (Exception e) { - Log.e(LOG_NAME, "Bad request.", e); - } - } - - public Collection getUsers() throws IOException { - Collection users = new ArrayList<>(); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(new TypeToken>(){}.getType(), new UsersTypeAdapter(context)) - .create(); - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - Response> response = service.getUsers().execute(); - - if (response.isSuccessful()) { - users = response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return users; - } - - public void getCaptcha(String username, String background, Callback callback) throws Exception { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - JsonObject json = new JsonObject(); - json.addProperty("username", username); - json.addProperty("background", background); - UserService service = retrofit.create(UserService.class); - service.signup(json).enqueue(callback); - } - - public void verifyUser( - String displayname, - String email, - String phone, - String password, - String captchaText, - String token, - Callback callback) throws Exception { - JsonObject user; - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - JsonObject json = new JsonObject(); - json.addProperty("displayName", displayname); - json.addProperty("email", email); - json.addProperty("phone", phone); - json.addProperty("password", password); - json.addProperty("passwordconfirm", password); - json.addProperty("captchaText", captchaText); - UserService service = retrofit.create(UserService.class); - service.signupVerify(String.format("Bearer %s", token), json).enqueue(callback); - } - - public User getUser(String userId) throws IOException { - User user = null; - - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(new TypeToken(){}.getType(), new UserTypeAdapter(context)) - .create(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - Response response = service.getUser(userId).execute(); - - if (response.isSuccessful()) { - user = response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - - return user; - } - - public User addRecentEvent(User user, Event event) throws IOException { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(new TypeToken(){}.getType(), new UserTypeAdapter(context)) - .create(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - Response response = service.addRecentEvent(user.getRemoteId(), event.getRemoteId()).execute(); - - if (response.isSuccessful()) { - return response.body(); - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - - return null; - } - } - - public User createAvatar(String avatarPath) { - try { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - Gson gson = new GsonBuilder() - .registerTypeAdapter(new TypeToken(){}.getType(), new UserTypeAdapter(context)) - .create(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create(gson)) - .client(HttpClientManager.getInstance().httpClient()) - .build(); - - UserService service = retrofit.create(UserService.class); - - Map parts = new HashMap<>(); - File avatar = new File(avatarPath); - String mimeType = MediaUtility.getMimeType(avatarPath); - RequestBody fileBody = RequestBody.create(MediaType.parse(mimeType), avatar); - parts.put("avatar\"; filename=\"" + avatar.getName() + "\"", fileBody); - - Response response = service.createAvatar(parts).execute(); - - if (response.isSuccessful()) { - User user = response.body(); - - UserHelper userHelper = UserHelper.getInstance(context); - User currentUser = userHelper.readCurrentUser(); - currentUser.setAvatarUrl(user.getAvatarUrl()); - currentUser.setLastModified(new Date(currentUser.getLastModified().getTime() + 1)); - UserHelper.getInstance(context).update(currentUser); - - userHelper.setAvatarPath(currentUser, null); - - Log.d(LOG_NAME, "Updated user with remote_id " + user.getRemoteId()); - - return user; - } else { - Log.e(LOG_NAME, "Bad request."); - if (response.errorBody() != null) { - Log.e(LOG_NAME, response.errorBody().string()); - } - } - } catch (Exception e) { - Log.e(LOG_NAME, "Failure saving observation.", e); - } - - return null; - } - - public void changePassword(String username, String password, String newPassword, String newPasswordConfirm, Callback callback) { - String baseUrl = PreferenceManager.getDefaultSharedPreferences(context).getString(context.getString(R.string.serverURLKey), context.getString(R.string.serverURLDefaultValue)); - - OkHttpClient.Builder builder = HttpClientManager.getInstance().httpClient().newBuilder(); - builder.interceptors().clear(); - OkHttpClient client = builder.build(); - - Retrofit retrofit = new Retrofit.Builder() - .baseUrl(baseUrl) - .addConverterFactory(GsonConverterFactory.create()) - .client(client) - .build(); - - UserService service = retrofit.create(UserService.class); - - JsonObject json = new JsonObject(); - json.addProperty("username", username); - json.addProperty("password", password); - json.addProperty("newPassword", newPassword); - json.addProperty("newPasswordConfirm", newPasswordConfirm); - - service.changePassword(json).enqueue(callback); - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/Deserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/Deserializer.java deleted file mode 100644 index a21a03815..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/Deserializer.java +++ /dev/null @@ -1,14 +0,0 @@ -package mil.nga.giat.mage.sdk.jackson.deserializer; - -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class Deserializer { - - static JsonFactory factory = new JsonFactory(); - static ObjectMapper mapper = new ObjectMapper(); - - static { - factory.setCodec(mapper); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/GeometryDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/GeometryDeserializer.java deleted file mode 100644 index 1e47bddca..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/GeometryDeserializer.java +++ /dev/null @@ -1,205 +0,0 @@ -package mil.nga.giat.mage.sdk.jackson.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; - -import java.io.IOException; - -import mil.nga.sf.Geometry; -import mil.nga.sf.GeometryCollection; -import mil.nga.sf.LineString; -import mil.nga.sf.MultiLineString; -import mil.nga.sf.MultiPoint; -import mil.nga.sf.MultiPolygon; -import mil.nga.sf.Point; -import mil.nga.sf.Polygon; - -public class GeometryDeserializer extends Deserializer { - - public Geometry parseGeometry(JsonParser parser) throws IOException { - if (parser.getCurrentToken() != JsonToken.START_OBJECT) return null; - - String typeName = null; - ArrayNode coordinates = null; - while (parser.nextToken() != JsonToken.END_OBJECT) { - String name = parser.getCurrentName(); - if ("type".equals(name)) { - parser.nextToken(); - typeName = parser.getText(); - } else if ("coordinates".equals(name)) { - parser.nextToken(); - coordinates = parser.readValueAsTree(); - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - - if (typeName == null) { - throw new IOException("'type' not present"); - } - - Geometry geometry = null; - switch (typeName) { - case "Point": - geometry = toPoint(coordinates); - break; - case "MultiPoint": - geometry = toMultiPoint(coordinates); - break; - case "LineString": - geometry = toLineString(coordinates); - break; - case "MultiLineString": - geometry = toMultiLineString(coordinates); - break; - case "Polygon": - geometry = toPolygon(coordinates); - break; - case "MultiPolygon": - geometry = toMultiPolygon(coordinates); - break; - case "GeometryCollection": - geometry = toGeometryCollection(coordinates); - break; - default: - throw new IOException("'type' not supported: " + typeName); - } - - return geometry; - } - - /** - * Convert a node to a Point - * - * @param node Point node - * @return Point - * @throws IOException - */ - public Point toPoint(JsonNode node) throws IOException { - double x = node.get(0).asDouble(); - double y = node.get(1).asDouble(); - Point point = new Point(x, y); - return point; - } - - /** - * Convert a node to a MultiPoint - * - * @param node MultiPoint node - * @return MultiPoint - * @throws IOException - */ - public MultiPoint toMultiPoint(JsonNode node) throws IOException { - - MultiPoint multiPoint = new MultiPoint(); - - for (int i = 0; i < node.size(); ++i) { - Point point = toPoint(node.get(i)); - multiPoint.addPoint(point); - } - - return multiPoint; - } - - /** - * Convert a node to a LineString - * - * @param node LineString node - * @return LineString - * @throws IOException - */ - public LineString toLineString(JsonNode node) throws IOException { - - LineString lineString = new LineString(); - - for (int i = 0; i < node.size(); ++i) { - Point point = toPoint(node.get(i)); - lineString.addPoint(point); - } - - return lineString; - } - - /** - * Convert a node to a MultiLineString - * - * @param node MultiLineString node - * @return MultiLineString - * @throws IOException - */ - public MultiLineString toMultiLineString(JsonNode node) throws IOException { - - MultiLineString multiLineString = new MultiLineString(); - - for (int i = 0; i < node.size(); ++i) { - LineString lineString = toLineString(node.get(i)); - multiLineString.addLineString(lineString); - } - - return multiLineString; - } - - /** - * Convert a node to a Polygon - * - * @param node Polygon node - * @return Polygon - * @throws IOException - */ - public Polygon toPolygon(JsonNode node) throws IOException { - - Polygon polygon = new Polygon(); - - LineString polygonLineString = toLineString(node.get(0)); - polygon.addRing(polygonLineString); - - for (int i = 1; i < node.size(); i++) { - LineString holeLineString = toLineString(node.get(i)); - polygon.addRing(holeLineString); - } - - return polygon; - } - - /** - * Convert a node to a MultiPolygon - * - * @param node MultiPolygon node - * @return MultiPolygon - * @throws IOException - */ - public MultiPolygon toMultiPolygon(JsonNode node) throws IOException { - - MultiPolygon multiPolygon = new MultiPolygon(); - - for (int i = 0; i < node.size(); i++) { - Polygon polygon = toPolygon(node.get(i)); - multiPolygon.addPolygon(polygon); - } - - return multiPolygon; - } - - /** - * Convert a node to a GeometryCollection - * - * @param node GeometryCollection node - * @return GeometryCollection - * @throws IOException - */ - public GeometryCollection toGeometryCollection(JsonNode node) throws IOException { - - GeometryCollection geometryCollection = new GeometryCollection(); - - for (int i = 0; i < node.size(); i++) { - Geometry geometry = parseGeometry(node.get(i).traverse()); - geometryCollection.addGeometry(geometry); - } - - return geometryCollection; - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/LocationDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/LocationDeserializer.java deleted file mode 100644 index 25943c820..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/LocationDeserializer.java +++ /dev/null @@ -1,179 +0,0 @@ -package mil.nga.giat.mage.sdk.jackson.deserializer; - -import android.util.Log; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; -import java.text.DateFormat; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import mil.nga.giat.mage.sdk.datastore.location.Location; -import mil.nga.giat.mage.sdk.datastore.location.LocationProperty; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory; - -public class LocationDeserializer extends Deserializer { - - private static final String LOG_NAME = LocationDeserializer.class.getName(); - - private final GeometryDeserializer geometryDeserializer = new GeometryDeserializer(); - - private Event event = null; - - public LocationDeserializer(Event event) { - this.event = event; - } - - public List parseUserLocations(InputStream is) throws IOException { - JsonParser parser = factory.createParser(is); - - List locations = new ArrayList<>(); - - if (parser.nextToken() != JsonToken.START_ARRAY) - return locations; - - while (parser.nextToken() != JsonToken.END_ARRAY) { - locations.addAll(parseUserLocations(parser)); - } - - parser.close(); - - return locations; - } - - private Collection parseUserLocations(JsonParser parser) throws IOException { - Collection locations = new ArrayList<>(); - - if (parser.getCurrentToken() != JsonToken.START_OBJECT) - return locations; - - while (parser.nextToken() != JsonToken.END_OBJECT) { - String name = parser.getCurrentName(); - if ("locations".equals(name)) { - locations.addAll(parseLocations(parser)); - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - - return locations; - } - - public List parseLocations(InputStream is) throws IOException { - JsonParser parser = factory.createParser(is); - - List locations = new ArrayList<>(); - locations.addAll(parseLocations(parser)); - parser.close(); - - return locations; - } - - private Collection parseLocations(JsonParser parser) throws IOException { - Collection locations = new ArrayList<>(); - parser.nextToken(); - while (parser.nextToken() != JsonToken.END_ARRAY) { - locations.add(parseLocation(parser)); - } - return locations; - } - - private Location parseLocation(JsonParser parser) throws IOException { - Location location = new Location(); - location.setEvent(event); - - if (parser.getCurrentToken() != JsonToken.START_OBJECT) { - return location; - } - - String userId = null; - Collection properties = null; - while (parser.nextToken() != JsonToken.END_OBJECT) { - String name = parser.getCurrentName(); - parser.nextToken(); - if ("_id".equals(name)) { - location.setRemoteId(parser.getText()); - } else if ("type".equals(name)) { - location.setType(parser.getText()); - } else if ("geometry".equals(name)) { - location.setGeometry(geometryDeserializer.parseGeometry(parser)); - } else if ("properties".equals(name)) { - properties = parseProperties(parser, location); - } else if ("userId".equals(name)) { - userId = parser.getText(); - } else { - parser.skipChildren(); - } - } - - // don't set the user at this time, only the id. Set it later. - properties.add(new LocationProperty("userId", userId)); - location.setProperties(properties); - - Map propertiesMap = location.getPropertiesMap(); - - // timestamp is special pull it out of properties and set it at the top level - LocationProperty timestamp = propertiesMap.get("timestamp"); - if (timestamp != null) { - try { - Date d = ISO8601DateFormatFactory.ISO8601().parse(timestamp.getValue().toString()); - location.setTimestamp(d); - } catch (ParseException pe) { - Log.w(LOG_NAME, "Unable to parse date: " + timestamp + " for location: " + location.getRemoteId(), pe); - } - } - return location; - } - - private Collection parseProperties(JsonParser parser, Location location) throws IOException { - Collection properties = new ArrayList(); - while (parser.nextToken() != JsonToken.END_OBJECT) { - String key = parser.getCurrentName(); - JsonToken token = parser.nextToken(); - if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) { - parser.skipChildren(); - } else { - Serializable value = parser.getText(); - if(token.isNumeric()) { - switch (parser.getNumberType()) { - case BIG_DECIMAL: - break; - case BIG_INTEGER: - break; - case DOUBLE: - value = parser.getDoubleValue(); - break; - case FLOAT: - value = parser.getFloatValue(); - break; - case INT: - value = parser.getIntValue(); - break; - case LONG: - value = parser.getLongValue(); - break; - default: - break; - } - } else if(token.isBoolean()) { - value = parser.getBooleanValue(); - } - LocationProperty property = new LocationProperty(key, value); - property.setLocation(location); - properties.add(property); - } - } - - return properties; - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/StaticFeatureDeserializer.java b/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/StaticFeatureDeserializer.java deleted file mode 100644 index b50d6b8c4..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/jackson/deserializer/StaticFeatureDeserializer.java +++ /dev/null @@ -1,94 +0,0 @@ -package mil.nga.giat.mage.sdk.jackson.deserializer; - -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import mil.nga.giat.mage.sdk.datastore.layer.Layer; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeature; -import mil.nga.giat.mage.sdk.datastore.staticfeature.StaticFeatureProperty; - -public class StaticFeatureDeserializer extends Deserializer { - - private final GeometryDeserializer geometryDeserializer = new GeometryDeserializer(); - - public List parseStaticFeatures(InputStream is, Layer layer) throws IOException { - List features = new ArrayList(); - - JsonParser parser = factory.createParser(is); - parser.nextToken(); - - if (parser.getCurrentToken() != JsonToken.START_OBJECT) { - return features; - } - - while (parser.nextToken() != JsonToken.END_OBJECT) { - String name = parser.getCurrentName(); - if ("features".equals(name)) { - parser.nextToken(); - while (parser.nextToken() != JsonToken.END_ARRAY) { - StaticFeature feature = parseFeature(parser); - feature.setLayer(layer); - features.add(feature); - } - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - - parser.close(); - return features; - } - - private StaticFeature parseFeature(JsonParser parser) throws IOException { - StaticFeature o = new StaticFeature(); - if (parser.getCurrentToken() != JsonToken.START_OBJECT) { - return o; - } - - while (parser.nextToken() != JsonToken.END_OBJECT) { - String name = parser.getCurrentName(); - if ("id".equals(name)) { - parser.nextToken(); - o.setRemoteId(parser.getText()); - } else if ("geometry".equals(name)) { - parser.nextToken(); - o.setGeometry(geometryDeserializer.parseGeometry(parser)); - } else if ("properties".equals(name)) { - parser.nextToken(); - o.setProperties(parseProperties(parser)); - } else { - parser.nextToken(); - parser.skipChildren(); - } - } - - return o; - } - - private Collection parseProperties(JsonParser parser) throws IOException { - Collection properties = new ArrayList<>(); - return parseProperties(parser, properties, ""); - } - - private Collection parseProperties(JsonParser parser, Collection properties, String keyPrefix) throws IOException { - while (parser.nextToken() != JsonToken.END_OBJECT) { - String key = keyPrefix + parser.getCurrentName().toLowerCase(); - if (parser.nextToken() == JsonToken.START_OBJECT) { - parseProperties(parser, properties, key); - } else { - String value = parser.getText(); - properties.add(new StaticFeatureProperty(key, value)); - } - } - - return properties; - } - -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationStatus.java b/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationStatus.java deleted file mode 100644 index a1718069d..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationStatus.java +++ /dev/null @@ -1,75 +0,0 @@ -package mil.nga.giat.mage.sdk.login; - -/** - * Contains information from resulting authentication - */ -public class AuthenticationStatus { - - public enum Status { - SUCCESSFUL_AUTHENTICATION, - DISCONNECTED_AUTHENTICATION, - ACCOUNT_CREATED, - FAILED_AUTHENTICATION, - } - - /** - * Request was successful or not - */ - private Status status = Status.FAILED_AUTHENTICATION; - - /** - * JSON Web Token used for authorization - */ - private String token; - - /** - * Message associated with authentication - */ - private String message; - - private AuthenticationStatus() { - - } - - public final Status getStatus() { - return status; - } - - public final String getToken() { - return token; - } - - public final String getMessage() { - return message; - } - - public static class Builder { - private final Status status; - private String token = ""; - private String message = null; - - public Builder(Status status) { - this.status = status; - } - - public Builder token(String token) { - this.token = token; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public AuthenticationStatus build() { - AuthenticationStatus authentication = new AuthenticationStatus(); - authentication.status = this.status; - authentication.token = this.token; - authentication.message = this.message; - - return authentication; - } - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationTask.java b/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationTask.java deleted file mode 100644 index eaef59df8..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthenticationTask.java +++ /dev/null @@ -1,144 +0,0 @@ -package mil.nga.giat.mage.sdk.login; - -import static mil.nga.giat.mage.sdk.login.AuthenticationStatus.Status.ACCOUNT_CREATED; -import static mil.nga.giat.mage.sdk.login.AuthenticationStatus.Status.DISCONNECTED_AUTHENTICATION; -import static mil.nga.giat.mage.sdk.login.AuthenticationStatus.Status.FAILED_AUTHENTICATION; -import static mil.nga.giat.mage.sdk.login.AuthenticationStatus.Status.SUCCESSFUL_AUTHENTICATION; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.preference.PreferenceManager; -import android.util.Log; - -import com.google.gson.JsonObject; - -import java.text.DateFormat; -import java.util.Date; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.http.resource.UserResource; -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory; -import mil.nga.giat.mage.sdk.utils.PasswordUtility; -import retrofit2.Response; - -/** - * Performs username/password authentication. - */ - -public class AuthenticationTask extends AsyncTask { - - public interface AuthenticationDelegate { - void onAuthenticationComplete(AuthenticationStatus status); - } - - private static final String LOG_NAME = AuthenticationTask.class.getName(); - - private final Context applicationContext; - private final AuthenticationDelegate delegate; - private boolean allowDisconnectedLogin = false; - - public AuthenticationTask(Context applicationContext, AuthenticationDelegate delegate) { - this(applicationContext, false, delegate); - } - - public AuthenticationTask(Context applicationContext, boolean allowDisconnectedLogin, AuthenticationDelegate delegate) { - this.applicationContext = applicationContext; - this.delegate = delegate; - this.allowDisconnectedLogin = allowDisconnectedLogin; - } - - /** - * @param params Should contain username, password; in that order. - * @return {@link AuthenticationStatus} - */ - @Override - protected AuthenticationStatus doInBackground(String... params) { - return login(params); - } - - private AuthenticationStatus login(String... params) { - String username = params[0]; - String password = params[1]; - String strategy = params[2]; - - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(applicationContext); - - // Try disconnected login - if (!ConnectivityUtility.isOnline(applicationContext) && allowDisconnectedLogin) { - try { - String oldUsername = sharedPreferences.getString(applicationContext.getString(R.string.usernameKey), null); - String oldPasswordHash = sharedPreferences.getString(applicationContext.getString(R.string.passwordHashKey), null); - if (oldUsername != null && oldPasswordHash != null && !oldPasswordHash.trim().isEmpty()) { - if (oldUsername.equals(username) && PasswordUtility.equal(password, oldPasswordHash)) { - // put the token expiration information in the shared preferences - long tokenExpirationLength = Math.max(sharedPreferences.getLong(applicationContext.getString(R.string.tokenExpirationLengthKey), 0), 0); - Date tokenExpiration = new Date(System.currentTimeMillis() + tokenExpirationLength); - sharedPreferences.edit().putString(applicationContext.getString(R.string.tokenExpirationDateKey), ISO8601DateFormatFactory.ISO8601().format(tokenExpiration)).apply(); - - return new AuthenticationStatus.Builder(DISCONNECTED_AUTHENTICATION).build(); - } - } - } catch (Exception e) { - Log.e(LOG_NAME, "Could not hash password", e); - } - - return new AuthenticationStatus.Builder(FAILED_AUTHENTICATION) - .message("We could not reach the server, please check your network connection and try again.") - .build(); - } - - try { - UserResource userResource = new UserResource(applicationContext); - JsonObject parameters = new JsonObject(); - parameters.addProperty("username", username); - parameters.addProperty("password", password); - Response signin = userResource.signin(strategy, parameters); - - if (signin != null) { - if (signin.isSuccessful()) { - JsonObject json = signin.body(); - String token = json.get("token").getAsString(); - - return new AuthenticationStatus.Builder(SUCCESSFUL_AUTHENTICATION) - .token(token) - .build(); - } else if ( signin.code() == 403) { - String errorMessage = "User account is not approved, please contact your MAGE administrator to approve your account."; - if (signin.errorBody() != null) { - errorMessage = signin.errorBody().string(); - } - - return new AuthenticationStatus.Builder(ACCOUNT_CREATED) - .message(errorMessage) - .build(); - } else { - String errorMessage = "Please check your username and password and try again."; - if (signin.errorBody() != null) { - errorMessage = signin.errorBody().string(); - } - - return new AuthenticationStatus.Builder(FAILED_AUTHENTICATION) - .message(errorMessage) - .build(); - } - } else { - return new AuthenticationStatus.Builder(FAILED_AUTHENTICATION) - .message("Error connecting to server, please contact your MAGE administrator") - .build(); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Problem logging in.", e); - } - - return new AuthenticationStatus.Builder(FAILED_AUTHENTICATION).build(); - } - - @Override - protected void onPostExecute(AuthenticationStatus authenticationStatus) { - super.onPostExecute(authenticationStatus); - - delegate.onAuthenticationComplete(authenticationStatus); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationStatus.java b/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationStatus.java deleted file mode 100644 index 98d9236dd..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationStatus.java +++ /dev/null @@ -1,108 +0,0 @@ -package mil.nga.giat.mage.sdk.login; - -import java.util.Date; - -import mil.nga.giat.mage.sdk.datastore.user.User; - -/** - * Contains information from resulting authentication - */ -public class AuthorizationStatus { - - public enum Status { - SUCCESSFUL_AUTHORIZATION, - FAILED_AUTHORIZATION, - FAILED_AUTHENTICATION, - INVALID_SERVER - } - - /** - * Request was successful or not - */ - private Status status; - - /** - * Contains information relevant to authorization, - * such as a user's api token - */ - private User user; - - /** - * MAGE API Token used for api access - */ - private String token; - - /** - * MAGE API Token expiration - */ - private Date tokenExpiration; - - /** - * Message associated with authentication - */ - private String message; - - private AuthorizationStatus() { - - } - - public final Status getStatus() { - return status; - } - - public final User getUser() { - return user; - } - - public final String getToken() { - return token; - } - - public final Date getTokenExpiration() { - return tokenExpiration; - } - - public final String getMessage() { - return message; - } - - public static class Builder { - private final Status status; - private User user; - private String token; - private Date tokenExpiration; - private String message = null; - - public Builder(Status status) { - this.status = status; - } - - public Builder authorization(User user, String token) { - this.user = user; - this.token = token; - return this; - } - - public Builder tokenExpiration(Date tokenExpiration) { - this.tokenExpiration = tokenExpiration; - return this; - } - - public Builder message(String message) { - this.message = message; - return this; - } - - public AuthorizationStatus build() { - AuthorizationStatus authentication = new AuthorizationStatus(); - authentication.status = this.status; - authentication.user = this.user; - authentication.token = this.token; - authentication.tokenExpiration = this.tokenExpiration; - authentication.message = this.message; - - return authentication; - } - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationTask.java b/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationTask.java deleted file mode 100644 index 50a1c67ea..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/login/AuthorizationTask.java +++ /dev/null @@ -1,103 +0,0 @@ -package mil.nga.giat.mage.sdk.login; - -import android.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import com.google.gson.JsonObject; -import com.google.gson.stream.JsonReader; - -import java.io.StringReader; -import java.text.DateFormat; -import java.text.ParseException; -import java.util.Date; - -import mil.nga.giat.mage.network.gson.user.UserTypeAdapter; -import mil.nga.giat.mage.sdk.Compatibility; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.http.resource.DeviceResource; -import mil.nga.giat.mage.sdk.utils.DeviceUuidFactory; -import mil.nga.giat.mage.sdk.utils.ISO8601DateFormatFactory; -import retrofit2.Response; - -/** - * Performs login to specified oauth server. - * - * @author newmanw - * - */ -public class AuthorizationTask extends AsyncTask { - - public interface AuthorizationDelegate { - void onAuthorizationComplete(AuthorizationStatus status); - } - - private static final String LOG_NAME = AuthorizationTask.class.getName(); - - private final Context applicationContext; - private final AuthorizationDelegate delegate; - private final UserTypeAdapter userTypeAdapter; - - public AuthorizationTask(Context applicationContext, AuthorizationDelegate delegate) { - this.applicationContext = applicationContext; - this.delegate = delegate; - userTypeAdapter = new UserTypeAdapter(applicationContext); - } - - /** - * Called from execute - * - * @param params Should contain authentication strategy and authentication token - */ - @Override - protected AuthorizationStatus doInBackground(String... params) { - String jwt = params[0]; - - try { - DeviceResource deviceResource = new DeviceResource(applicationContext); - String uid = new DeviceUuidFactory(applicationContext).getDeviceUuid().toString(); - Response authorizeResponse = deviceResource.authorize(jwt, uid); - if (authorizeResponse == null || !authorizeResponse.isSuccessful()) { - int code = authorizeResponse == null ? 401 : authorizeResponse.code(); - AuthorizationStatus.Status status = code == 403 ? AuthorizationStatus.Status.FAILED_AUTHORIZATION : AuthorizationStatus.Status.FAILED_AUTHENTICATION; - return new AuthorizationStatus.Builder(status).build(); - } - - JsonObject authorization = authorizeResponse.body(); - - // check server api version to ensure compatibility before continuing - JsonObject serverVersion = authorization.get("api").getAsJsonObject().get("version").getAsJsonObject(); - if (!Compatibility.Companion.isCompatibleWith(serverVersion.get("major").getAsInt(), serverVersion.get("minor").getAsInt())) { - Log.e(LOG_NAME, "Server version not compatible"); - return new AuthorizationStatus.Builder(AuthorizationStatus.Status.INVALID_SERVER).build(); - } - - // Successful login, put the token information in the shared preferences - String token = authorization.get("token").getAsString(); - Date tokenExpiration = null; - try { - tokenExpiration = ISO8601DateFormatFactory.ISO8601().parse(authorization.get("expirationDate").getAsString().trim()); - } catch (ParseException e) { - Log.e(LOG_NAME, "Problem parsing token expiration date.", e); - } - - JsonObject userJson = authorization.getAsJsonObject("user"); - JsonReader reader = new JsonReader(new StringReader(userJson.toString())); - User user = userTypeAdapter.read(reader); - return new AuthorizationStatus.Builder(AuthorizationStatus.Status.SUCCESSFUL_AUTHORIZATION) - .authorization(user, token) - .tokenExpiration(tokenExpiration) - .build(); - } catch (Exception e) { - Log.e(LOG_NAME, "Problem with authorization attempt", e); - return new AuthorizationStatus.Builder(AuthorizationStatus.Status.FAILED_AUTHORIZATION).build(); - } - } - - @Override - protected void onPostExecute(AuthorizationStatus status) { - super.onPostExecute(status); - - delegate.onAuthorizationComplete(status); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/login/RecentEventTask.java b/mage/src/main/java/mil/nga/giat/mage/sdk/login/RecentEventTask.java deleted file mode 100644 index f76aaea1c..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/login/RecentEventTask.java +++ /dev/null @@ -1,59 +0,0 @@ -package mil.nga.giat.mage.sdk.login; - -import android.content.Context; -import android.os.AsyncTask; -import android.util.Log; - -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.datastore.user.Event; -import mil.nga.giat.mage.sdk.datastore.user.EventHelper; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.datastore.user.UserHelper; -import mil.nga.giat.mage.sdk.http.resource.UserResource; - -/** - * Updates user's recent event - */ -public class RecentEventTask extends AsyncTask { - - private static final String LOG_NAME = RecentEventTask.class.getName(); - - private final Context applicationContext; - private final UserHelper userHelper; - private final UserResource userResource; - - public RecentEventTask(Context applicationContext) { - this.applicationContext = applicationContext; - userHelper = UserHelper.getInstance(applicationContext); - userResource = new UserResource(applicationContext); - } - - @Override - protected Boolean doInBackground(String... params) { - // get the user's recent event - String userRecentEventRemoteId = params[0]; - - try { - Event userRecentEvent = EventHelper.getInstance(applicationContext).read(userRecentEventRemoteId); - - // tell the server and update the local store - if (ConnectivityUtility.isOnline(applicationContext)) { - User currentUser = userHelper.readCurrentUser(); - - User user = userResource.addRecentEvent(currentUser, userRecentEvent); - if (user != null) { - return true; - } - } - } catch(Exception e) { - Log.e(LOG_NAME, "Unable to get current event.", e); - } - - return false; - } - - @Override - protected void onPostExecute(Boolean aBoolean) { - super.onPostExecute(aBoolean); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/EditTextSummaryPreference.java b/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/EditTextSummaryPreference.java deleted file mode 100644 index a48625a57..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/EditTextSummaryPreference.java +++ /dev/null @@ -1,28 +0,0 @@ -package mil.nga.giat.mage.sdk.preferences; - -import android.content.Context; -import androidx.preference.EditTextPreference; -import android.util.AttributeSet; - -/** - * Created by wnewman on 12/15/15. - */ -public class EditTextSummaryPreference extends EditTextPreference { - public EditTextSummaryPreference(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - public EditTextSummaryPreference(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public EditTextSummaryPreference(Context context) { - super(context); - } - - @Override - public CharSequence getSummary() { - String summary = super.getSummary().toString(); - return String.format(summary, getText()); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/ServerApi.java b/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/ServerApi.java deleted file mode 100644 index b31e25db8..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/preferences/ServerApi.java +++ /dev/null @@ -1,176 +0,0 @@ -package mil.nga.giat.mage.sdk.preferences; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Iterator; - -import mil.nga.giat.mage.R; -import mil.nga.giat.mage.sdk.Compatibility; -import mil.nga.giat.mage.sdk.http.resource.ApiResource; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; - -/** - * Created by wnewman on 1/4/18. - */ - -public class ServerApi { - public interface ServerApiListener { - void onApi(boolean valid, Exception error); - } - - private static final String LOG_NAME = ServerApi.class.getName(); - private static final String SERVER_API_PREFERENCE_PREFIX_REGEX = "^g[A-Z]\\w*"; - private static final String SERVER_API_PREFERENCE_PREFIX = "g"; - private static final String SERVER_API_AUTHENTICATION_STRATEGIES_KEY = "authenticationStrategies"; - - private final Context context; - - public ServerApi(Context context) { - this.context = context; - } - - public void validateServerApi(final String url, final ServerApiListener apiListener) { - ApiResource apiResource = new ApiResource(context); - - apiResource.getApi(url, new Callback() { - @Override - public void onResponse(Call call, Response response) { - try { - if (response.isSuccessful()) { - JSONObject apiJson = new JSONObject(response.body().string()); - removeValues(); - populateValues(SERVER_API_PREFERENCE_PREFIX, apiJson); - parseAuthenticationStrategies(apiJson); - - String message = null; - boolean isValid = isApiValid(); - if (!isValid) { - message = "Application is not compatible with server. Please upgrade your application or talk to your MAGE administrator"; - } - apiListener.onApi(isValid, null); - } else { - apiListener.onApi(false, null); - } - } catch (Exception e) { - Log.e(LOG_NAME, "Problem reading server api settings: " + url, e); - apiListener.onApi(false, e); - } - } - - @Override - public void onFailure(Call call, final Throwable t) { - Log.e(LOG_NAME, "Problem reading server api settings: " + url, t); - apiListener.onApi(false, new Exception(t)); - } - }); - } - - private boolean isApiValid() { - // check versions - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - - Integer majorVersion = null; - if (sharedPreferences.contains(context.getString(R.string.serverVersionMajorKey))) { - majorVersion = sharedPreferences.getInt(context.getString(R.string.serverVersionMajorKey), 0); - } - - Integer minorVersion = null; - if (sharedPreferences.contains(context.getString(R.string.serverVersionMinorKey))) { - minorVersion = sharedPreferences.getInt(context.getString(R.string.serverVersionMinorKey), 0); - } - - return Compatibility.Companion.isCompatibleWith(majorVersion, minorVersion); - } - - private void parseAuthenticationStrategies(JSONObject json) { - try { - Object value = json.get(SERVER_API_AUTHENTICATION_STRATEGIES_KEY); - if (value instanceof JSONObject) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(context.getResources().getString(R.string.authenticationStrategiesKey), value.toString()); - editor.apply(); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - /** - * Flattens the json from the server and puts key, value pairs in the DefaultSharedPreferences - * - * @param sharedPreferenceName preference name - * @param json json value - */ - private void populateValues(String sharedPreferenceName, JSONObject json) { - Iterator iter = json.keys(); - while (iter.hasNext()) { - String key = iter.next(); - try { - Object value = json.get(key); - if (value instanceof JSONObject) { - populateValues(sharedPreferenceName + Character.toUpperCase(key.charAt(0)) + ((key.length() > 1) ? key.substring(1) : ""), (JSONObject) value); - } else { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = sharedPreferences.edit(); - String keyString = sharedPreferenceName + Character.toUpperCase(key.charAt(0)) + ((key.length() > 1) ? key.substring(1) : ""); - Log.i(LOG_NAME, keyString + " is " + sharedPreferences.getAll().get(keyString) + ". Setting it to " + value + "."); - - if(value instanceof Number) { - if(value instanceof Long) { - editor.putLong(keyString, (Long)value); - } else if(value instanceof Float) { - editor.putFloat(keyString, (Float)value); - } else if(value instanceof Double) { - editor.putFloat(keyString, ((Double)value).floatValue()); - } else if(value instanceof Integer) { - editor.putInt(keyString, (Integer) value); - } else if(value instanceof Short) { - editor.putInt(keyString, ((Short)value).intValue()); - } else { - Log.e(LOG_NAME, keyString + " with value " + value + " is not of valid number type. Skipping this key-value pair."); - } - } else if(value instanceof Boolean) { - editor.putBoolean(keyString, (Boolean) value); - } else if(value instanceof String) { - editor.putString(keyString, (String) value); - } else if(value instanceof Character) { - editor.putString(keyString, Character.toString((Character)value)); - } else { - // don't know what type this is, just use toString - try { - editor.putString(keyString, value.toString()); - } catch(Exception e) { - Log.e(LOG_NAME, keyString + " with value " + value + " is not of valid type. Skipping this key-value pair."); - } - } - - editor.commit(); - } - } catch (JSONException je) { - je.printStackTrace(); - } - } - } - - private void removeValues() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - SharedPreferences.Editor editor = sharedPreferences.edit(); - for (String key : sharedPreferences.getAll().keySet()) { - if (key.matches(SERVER_API_PREFERENCE_PREFIX_REGEX)) { - editor.remove(key); - } - } - - editor.commit(); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/profile/UpdateProfileTask.java b/mage/src/main/java/mil/nga/giat/mage/sdk/profile/UpdateProfileTask.java deleted file mode 100644 index f54ee257f..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/profile/UpdateProfileTask.java +++ /dev/null @@ -1,106 +0,0 @@ -package mil.nga.giat.mage.sdk.profile; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Bitmap.CompressFormat; -import android.graphics.BitmapFactory; -import android.os.AsyncTask; -import android.util.Log; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.math.BigInteger; -import java.security.SecureRandom; - -import mil.nga.giat.mage.sdk.connectivity.ConnectivityUtility; -import mil.nga.giat.mage.sdk.datastore.user.User; -import mil.nga.giat.mage.sdk.http.resource.UserResource; -import mil.nga.giat.mage.sdk.utils.MediaUtility; - -public class UpdateProfileTask extends AsyncTask { - - private final User user; - private final Context context; - private final UserResource userResource; - - private static final SecureRandom random = new SecureRandom(); - - private static final String LOG_NAME = UpdateProfileTask.class.getName(); - - public UpdateProfileTask(User user, Context context) { - this.user = user; - this.context = context; - userResource = new UserResource(context); - } - - - @Override - protected User doInBackground(String... params) { - - // get inputs - String fileToUpload = params[0]; - - // Make sure you have connectivity - if (!ConnectivityUtility.isOnline(context)) { - // TODO Auto-generated method stub - return user; - } - - File stageDir = MediaUtility.getMediaStageDirectory(context); - File inFile = new File(fileToUpload); - // add random string to the front of the filename to avoid conflicts - File stagedFile = new File(stageDir, new BigInteger(30, random).toString(32) + new File(fileToUpload).getName()); - - Log.d(LOG_NAME, "Staging file: " + stagedFile.getAbsolutePath()); - if (stagedFile.getAbsolutePath().equalsIgnoreCase(fileToUpload)) { - Log.d(LOG_NAME, "Attachment is already staged. Nothing to do."); - return user; - } - - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inPreferredConfig = Bitmap.Config.RGB_565; - options.inSampleSize = 2; - Bitmap bitmap = BitmapFactory.decodeFile(inFile.getAbsolutePath(), options); - - // Scale file - Integer inWidth = bitmap.getWidth(); - Integer inHeight = bitmap.getHeight(); - - Integer outImageSize = 512; - Integer outWidth = outImageSize; - Integer outHeight = outImageSize; - - if (inWidth > inHeight) { - outHeight = ((Double) ((inHeight.doubleValue() / inWidth.doubleValue()) * outImageSize.doubleValue())).intValue(); - } else if (inWidth < inHeight) { - outWidth = ((Double) ((inWidth.doubleValue() / inHeight.doubleValue()) * outImageSize.doubleValue())).intValue(); - } - bitmap = Bitmap.createScaledBitmap(bitmap, outWidth, outHeight, true); - - try { - OutputStream out = new FileOutputStream(stagedFile); - bitmap.compress(CompressFormat.JPEG, 100, out); - - out.flush(); - out.close(); - bitmap.recycle(); - MediaUtility.copyExifData(inFile, stagedFile); - } catch (Exception e) { - Log.e(LOG_NAME, "Failed to upload file", e); - } - - Log.i(LOG_NAME, "Pushing profile picture " + stagedFile); - - User user = userResource.createAvatar(stagedFile.getAbsolutePath()); - return user; - } - - - @Override - protected void onPostExecute(User user) { - super.onPostExecute(user); - - Log.i(LOG_NAME, "updated user avatar"); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/screen/ScreenChangeReceiver.java b/mage/src/main/java/mil/nga/giat/mage/sdk/screen/ScreenChangeReceiver.java deleted file mode 100644 index d530d68ef..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/screen/ScreenChangeReceiver.java +++ /dev/null @@ -1,52 +0,0 @@ -package mil.nga.giat.mage.sdk.screen; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import java.util.Collection; -import java.util.concurrent.CopyOnWriteArrayList; - -import mil.nga.giat.mage.sdk.event.IEventDispatcher; -import mil.nga.giat.mage.sdk.event.IScreenEventListener; - -public class ScreenChangeReceiver extends BroadcastReceiver implements IEventDispatcher { - - /** - * Singleton. - */ - private static ScreenChangeReceiver mScreenChangeReceiver; - - /** - * Do not use! - */ - public ScreenChangeReceiver() { - - } - - public static ScreenChangeReceiver getInstance() { - if (mScreenChangeReceiver == null) { - mScreenChangeReceiver = new ScreenChangeReceiver(); - } - return mScreenChangeReceiver; - } - - private static final Collection listeners = new CopyOnWriteArrayList(); - - @Override - public void onReceive(final Context context, final Intent intent) { - for (IScreenEventListener listener : listeners) { - listener.onScreenOn(); - } - } - - @Override - public boolean addListener(IScreenEventListener listener) { - return listeners.add(listener); - } - - @Override - public boolean removeListener(IScreenEventListener listener) { - return listeners.remove(listener); - } -} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.java b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.java deleted file mode 100644 index 76f31e779..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.java +++ /dev/null @@ -1,64 +0,0 @@ -package mil.nga.giat.mage.sdk.utils; - -import android.util.Log; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.nio.ByteOrder; - -import mil.nga.sf.Geometry; -import mil.nga.sf.util.ByteReader; -import mil.nga.sf.util.ByteWriter; -import mil.nga.sf.wkb.GeometryReader; -import mil.nga.sf.wkb.GeometryWriter; - -/** - * Geometry Utilities - * - * @author osbornb - */ -public class GeometryUtility { - - /** - * Convert well-known binary bytes to a Geometry - * - * @param geometryBytes geometry bytes - * @return geometry - */ - @Nullable - public static Geometry toGeometry(byte[] geometryBytes) { - ByteReader reader = new ByteReader(geometryBytes); - reader.setByteOrder(ByteOrder.BIG_ENDIAN); - Geometry geometry = null; - try { - geometry = GeometryReader.readGeometry(reader); - } catch (IOException e) { - e.printStackTrace(); - } - - return geometry; - } - - /** - * Convert a Geometry to well-known binary bytes - * - * @param geometry geometry - * @return well-known binary bytes - */ - public static byte[] toGeometryBytes(Geometry geometry) { - byte[] bytes = null; - ByteWriter writer = new ByteWriter(); - try { - writer.setByteOrder(ByteOrder.BIG_ENDIAN); - GeometryWriter.writeGeometry(writer, geometry); - bytes = writer.getBytes(); - } catch (IOException e) { - Log.e(GeometryUtility.class.getSimpleName(), "Problem reading observation.", e); - } finally { - writer.close(); - } - return bytes; - } - -} diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.kt b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.kt new file mode 100644 index 000000000..6609dd5ed --- /dev/null +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/GeometryUtility.kt @@ -0,0 +1,29 @@ +package mil.nga.giat.mage.sdk.utils + +import mil.nga.sf.Geometry +import mil.nga.sf.util.ByteReader +import mil.nga.sf.util.ByteWriter +import mil.nga.sf.wkb.GeometryReader +import mil.nga.sf.wkb.GeometryWriter +import java.lang.Exception +import java.nio.ByteOrder + +fun ByteArray.toGeometry(): Geometry? { + val reader = ByteReader(this) + reader.byteOrder = ByteOrder.BIG_ENDIAN + + return try { + GeometryReader.readGeometry(reader) + } catch (e: Exception) { null } +} + +fun Geometry.toBytes(): ByteArray { + val writer = ByteWriter() + return try { + writer.byteOrder = ByteOrder.BIG_ENDIAN + GeometryWriter.writeGeometry(writer, this) + writer.bytes + } finally { + writer.close() + } +} \ No newline at end of file diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/MediaUtility.java b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/MediaUtility.java index c5a0afd51..1c492b648 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/MediaUtility.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/MediaUtility.java @@ -1,21 +1,11 @@ package mil.nga.giat.mage.sdk.utils; -import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.Bitmap.Config; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.RectF; import android.net.Uri; -import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; @@ -25,18 +15,8 @@ import com.google.common.io.ByteStreams; import org.apache.commons.lang3.StringUtils; -import org.apache.sanselan.Sanselan; -import org.apache.sanselan.common.IImageMetadata; -import org.apache.sanselan.formats.jpeg.JpegImageMetadata; -import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter; -import org.apache.sanselan.formats.tiff.TiffImageMetadata; -import org.apache.sanselan.formats.tiff.constants.TiffConstants; -import org.apache.sanselan.formats.tiff.write.TiffOutputSet; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; + import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; @@ -66,14 +46,7 @@ public class MediaUtility { private static final String LOG_NAME = MediaUtility.class.getName(); - - private static int getPowerOfTwoForSampleRatio(double ratio){ - int k = Integer.highestOneBit((int)Math.floor(ratio)); - if(k==0) return 1; - else return k; - } - public static String getMimeType(String url) { String type = null; if(StringUtils.isBlank(url)) { @@ -114,8 +87,7 @@ public static void addImageToGallery(Context c, Uri contentUri) { c.sendBroadcast(mediaScanIntent); } - public static File - copyMediaFromUri(Context context, Uri uri) throws IOException { + public static File copyMediaFromUri(Context context, Uri uri) throws IOException { InputStream is = null; OutputStream os = null; try { @@ -164,64 +136,6 @@ public static void addImageToGallery(Context c, Uri contentUri) { } } - public static File createImageFile() throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "MAGE_" + timeStamp; - File directory = getPublicAttachmentsDirectory(Environment.DIRECTORY_PICTURES); - - return File.createTempFile( - imageFileName, - ".jpg", - directory - ); - } - - public static File createVideoFile() throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "MAGE_" + timeStamp; - File directory = getPublicAttachmentsDirectory(Environment.DIRECTORY_MOVIES); - - return File.createTempFile( - imageFileName, - ".mp4", - directory - ); - } - - public static File createAudioFile() throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "MAGE_" + timeStamp; - File directory = getPublicAttachmentsDirectory(Environment.DIRECTORY_MUSIC); - - return File.createTempFile( - imageFileName, - ".mp4", - directory - ); - } - - public static File createFile(String extension) throws IOException { - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "MAGE_" + timeStamp; - File directory = getPublicAttachmentsDirectory(Environment.DIRECTORY_DOWNLOADS); - - return File.createTempFile( - imageFileName, - "." + extension, - directory - ); - } - - public static File getPublicAttachmentsDirectory(String type) { - File directory = new File(Environment.getExternalStoragePublicDirectory(type), "MAGE"); - - if (!directory.exists()) { - directory.mkdirs(); - } - - return directory; - } - public static File getMediaStageDirectory(Context context) { File directory = new File(context.getFilesDir(), "media"); if (!directory.exists()) { @@ -230,17 +144,7 @@ public static File getMediaStageDirectory(Context context) { return directory; } - - public static File getAvatarDirectory(Context context) { - File directory = getMediaStageDirectory(context); - File avatarDirectory = new File(directory, "/user/avatars"); - if (!avatarDirectory.exists()) { - avatarDirectory.mkdirs(); - } - return avatarDirectory; - } - public static File getUserIconDirectory(Context context) { File directory = getMediaStageDirectory(context); File iconDirectory = new File(directory, "/user/icons"); @@ -572,177 +476,6 @@ public static String getDisplayName(Context context, Uri uri, String path) { return name; } - /** - * Get the display name from the URI and path - * - * @param context - * @param uri - * @return - */ - public static String getDisplayNameWithoutExtension(Context context, Uri uri) { - return getDisplayNameWithoutExtension(context, uri, null); - } - - /** - * Get the display name from the URI and path - * - * @param context - * @param uri - * @param path - * @return - */ - public static String getDisplayNameWithoutExtension(Context context, Uri uri, String path) { - - // Try to get the GeoPackage name - String name = getDisplayName(context, uri, path); - - // Remove the extension - if (name != null) { - int extensionIndex = name.lastIndexOf("."); - if (extensionIndex > -1) { - name = name.substring(0, extensionIndex); - } - } - - return name; - } - - public static String getFileAbsolutePath(Uri uri, Context c) - { - String fileName = null; - String scheme = uri.getScheme(); - if (scheme.equals("file")) { - fileName = uri.getPath(); - } - else if (scheme.equals("content")) { - Cursor cursor = null; - try { - String[] proj = { MediaStore.Images.Media.DATA }; - cursor = c.getContentResolver().query(uri, proj, null, null, null); - int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); - cursor.moveToFirst(); - return cursor.getString(column_index); - } catch (Exception e) { - Log.e(LOG_NAME, "Error reading content URI", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - } - return fileName; - } - - public static void copyExifData(File sourceFile, File destFile) { - String tempFileName = destFile.getAbsolutePath() + ".tmp"; - File tempFile = null; - OutputStream tempStream = null; - - try { - tempFile = new File(tempFileName); - TiffOutputSet sourceSet = getSanselanOutputSet(sourceFile); - - // Save data to destination - tempStream = new BufferedOutputStream(new FileOutputStream(tempFile)); - new ExifRewriter().updateExifMetadataLossless(destFile, tempStream, sourceSet); - tempStream.close(); - - if (destFile.delete()) { - tempFile.renameTo(destFile); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (tempStream != null) { - try { - tempStream.close(); - } catch (IOException e) { - } - } - - if (tempFile != null) { - if (tempFile.exists()) { - tempFile.delete(); - } - } - } - } - - private static TiffOutputSet getSanselanOutputSet(File jpegImageFile) throws Exception { - TiffImageMetadata exif = null; - TiffOutputSet outputSet = null; - - IImageMetadata metadata = Sanselan.getMetadata(jpegImageFile); - JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; - if (jpegMetadata != null) { - exif = jpegMetadata.getExif(); - - if (exif != null) { - outputSet = exif.getOutputSet(); - } - } - - // If JPEG file contains no EXIF metadata, create an empty set of EXIF metadata. Otherwise, use existing EXIF metadata to keep all other existing tags - if (outputSet == null) { - outputSet = new TiffOutputSet(exif == null ? TiffConstants.DEFAULT_TIFF_BYTE_ORDER : exif.contents.header.byteOrder); - } - - return outputSet; - } - - public static Bitmap resizeAndRoundCorners(Bitmap bitmap, int maxSize) { - boolean isLandscape = bitmap.getWidth() > bitmap.getHeight(); - - int newWidth, newHeight; - if (isLandscape) - { - newWidth = maxSize; - newHeight = Math.round(((float) newWidth / bitmap.getWidth()) * bitmap.getHeight()); - } else - { - newHeight = maxSize; - newWidth = Math.round(((float) newHeight / bitmap.getHeight()) * bitmap.getWidth()); - } - - Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, false); - - if (resizedBitmap != bitmap) - bitmap.recycle(); - - Bitmap roundedProfile = Bitmap.createBitmap(resizedBitmap.getWidth(), resizedBitmap - .getHeight(), Config.ARGB_8888); - - Canvas roundedCanvas = new Canvas(roundedProfile); - final int color = 0xff424242; - final Paint paint = new Paint(); - final Rect rect = new Rect(0, 0, roundedProfile.getWidth(), roundedProfile.getHeight()); - final RectF rectF = new RectF(rect); - final float roundPx = 7.0f; - - paint.setAntiAlias(true); - roundedCanvas.drawARGB(0, 0, 0, 0); - paint.setColor(color); - roundedCanvas.drawRoundRect(rectF, roundPx, roundPx, paint); - - paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); - roundedCanvas.drawBitmap(resizedBitmap, rect, rect, paint); - return roundedProfile; - } - - /** - * Get the file extension - * - * @param file - * @return - */ - public static String getFileExtension(File file) { - - String fileName = file.getName(); - String extension = getFileExtension(fileName); - - return extension; - } - /** * Get the file extension * @@ -791,83 +524,4 @@ public static String getNameWithoutExtension(String name) { return name; } - /** - * Copy a file to a file location - * - * @param copyFrom - * @param copyTo - * @throws IOException - */ - public static void copyFile(File copyFrom, File copyTo) throws IOException { - - InputStream from = new FileInputStream(copyFrom); - OutputStream to = new FileOutputStream(copyTo); - - copyStream(from, to); - } - - /** - * Copy an input stream to a file location - * - * @param copyFrom - * @param copyTo - * @throws IOException - */ - public static void copyStream(InputStream copyFrom, File copyTo) - throws IOException { - - OutputStream to = new FileOutputStream(copyTo); - - copyStream(copyFrom, to); - } - - /** - * Get the file bytes - * - * @param file - * @throws IOException - */ - public static byte[] fileBytes(File file) throws IOException { - - FileInputStream fis = new FileInputStream(file); - - return streamBytes(fis); - } - - /** - * Get the stream bytes - * - * @param stream - * @throws IOException - */ - public static byte[] streamBytes(InputStream stream) throws IOException { - - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - copyStream(stream, bytes); - - return bytes.toByteArray(); - } - - /** - * Copy an input stream to an output stream - * - * @param copyFrom - * @param copyTo - * @throws IOException - */ - public static void copyStream(InputStream copyFrom, OutputStream copyTo) - throws IOException { - - byte[] buffer = new byte[1024]; - int length; - while ((length = copyFrom.read(buffer)) > 0) { - copyTo.write(buffer, 0, length); - } - - copyTo.flush(); - copyTo.close(); - copyFrom.close(); - } - } diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/PasswordUtility.java b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/PasswordUtility.java index 1978692b7..8fa302da8 100644 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/PasswordUtility.java +++ b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/PasswordUtility.java @@ -4,56 +4,53 @@ import java.security.SecureRandom; +import javax.annotation.Nullable; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; -/** - * - * Used to hash passwords securely - * - * @author wiedemanns - */ public class PasswordUtility { private static final int iterations = 1000; private static final int saltLen = 32; private static final int desiredKeyLen = 256; - /** - * Computes a salted PBKDF2 hash of given plaintext password suitable for storing in a database. Empty passwords are not supported. - * - * @param password - * @return - * @throws Exception - */ - public static String getSaltedHash(String password) throws Exception { - byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen); - // store the salt with the password - return Base64.encodeToString(salt, Base64.NO_WRAP) + "$" + hash(password, salt); + public static @Nullable String getSaltedHash(String password) { + try { + byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen); + // store the salt with the password + return Base64.encodeToString(salt, Base64.NO_WRAP) + "$" + hash(password, salt); + } catch (Exception e) { + return null; + } } - public static boolean equal(String password, String hash) throws Exception { - if(hash == null || password == null) { + public static boolean equal(String password, String hash) { + if (hash == null || password == null) { return false; } - String[] saltAndPass = hash.split("\\$"); - if (saltAndPass.length != 2) { - throw new IllegalStateException("The stored password have the form 'salt$hash'"); + try { + String[] saltAndPass = hash.split("\\$"); + if (saltAndPass.length != 2) return false; + String hashOfInput = hash(password, Base64.decode(saltAndPass[0], Base64.NO_WRAP)); + return hashOfInput.equals(saltAndPass[1]); + } catch (Exception e) { + return false; } - String hashOfInput = hash(password, Base64.decode(saltAndPass[0], Base64.NO_WRAP)); - return hashOfInput.equals(saltAndPass[1]); } - // using PBKDF2 from Sun - private static String hash(String password, byte[] salt) throws Exception { + private static @Nullable String hash(String password, byte[] salt) { if (password == null || password.length() == 0) { - throw new IllegalArgumentException("Empty passwords are not supported."); + return null; + } + + try { + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + SecretKey key = f.generateSecret(new PBEKeySpec(password.toCharArray(), salt, iterations, desiredKeyLen)); + return Base64.encodeToString(key.getEncoded(), Base64.NO_WRAP); + } catch (Exception e) { + return null; } - SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - SecretKey key = f.generateSecret(new PBEKeySpec(password.toCharArray(), salt, iterations, desiredKeyLen) - ); - return Base64.encodeToString(key.getEncoded(), Base64.NO_WRAP); } } diff --git a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/UserUtility.java b/mage/src/main/java/mil/nga/giat/mage/sdk/utils/UserUtility.java deleted file mode 100644 index 59fcdffc9..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/sdk/utils/UserUtility.java +++ /dev/null @@ -1,66 +0,0 @@ -package mil.nga.giat.mage.sdk.utils; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.text.DateFormat; -import java.text.ParseException; -import java.util.Date; - -import mil.nga.giat.mage.R; - -/** - * Utility that currently deals mostly with the user's token information. - * - * @author wiedemanns - * - */ -public class UserUtility { - - private static final String LOG_NAME = UserUtility.class.getName(); - - private UserUtility() { - } - - private static UserUtility userUtility; - private static Context mContext; - - public static UserUtility getInstance(final Context context) { - if (context == null) { - return null; - } - if (userUtility == null) { - userUtility = new UserUtility(); - } - mContext = context; - return userUtility; - } - - // TODO token info is really a function of login type - // this should probably be in the auth module as something more generic, - // in case we ever go to a different login module - public synchronized final Boolean isTokenExpired() { - String tokenExpirationDateString = PreferenceManager.getDefaultSharedPreferences(mContext).getString(mContext.getString(R.string.tokenExpirationDateKey), null); - if (tokenExpirationDateString != null && !tokenExpirationDateString.isEmpty()) { - try { - return new Date().after(ISO8601DateFormatFactory.ISO8601().parse(tokenExpirationDateString)); - } catch (ParseException pe) { - Log.e(LOG_NAME, "Problem paring token date.", pe); - } - } - return true; - } - - // TODO token info is really a function of login type - // this should probably be in the auth module as something more generic, - // in case we ever go to a different login module - public synchronized final void clearTokenInformation() { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); - Editor editor = sharedPreferences.edit(); - editor.remove(mContext.getString(R.string.tokenKey)).commit(); - editor.remove(mContext.getString(R.string.tokenExpirationDateKey)).commit(); - } -} diff --git a/mage/src/main/java/mil/nga/giat/mage/wearable/InitializeMAGEWearBridge.java b/mage/src/main/java/mil/nga/giat/mage/wearable/InitializeMAGEWearBridge.java deleted file mode 100644 index 50a46f086..000000000 --- a/mage/src/main/java/mil/nga/giat/mage/wearable/InitializeMAGEWearBridge.java +++ /dev/null @@ -1,39 +0,0 @@ -package mil.nga.giat.mage.wearable; - -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; -import android.util.Log; - -import java.lang.reflect.Method; - -import mil.nga.giat.mage.R; - -public class InitializeMAGEWearBridge { - - private static final String LOG_NAME = InitializeMAGEWearBridge.class.getName(); - - public static Boolean startBridgeIfWearBuild(Context context) { - SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - Boolean isWearBuild = sharedPreferences.getBoolean(context.getString(R.string.isWearBuildKey), context.getResources().getBoolean(R.bool.isWearBuildDefaultValue)); - if (isWearBuild) { - final String MAGEWearBridgeClassName = "mil.nga.giat.mage.wearable.bridge.MAGEWearBridge"; - - try { - Class c = Class.forName(MAGEWearBridgeClassName); - Method getInstanceMethod = c.getMethod("getInstance", Context.class); - - Object mageWearBridge = getInstanceMethod.invoke(null, context); - - Method startBridgeMethod = c.getMethod("startBridge"); - startBridgeMethod.invoke(mageWearBridge); - - return true; - } catch (Exception e) { - Log.e(LOG_NAME, MAGEWearBridgeClassName + " is missing. Unable to start the bridge."); - return false; - } - } - return true; - } -} \ No newline at end of file diff --git a/mage/src/main/res/values/strings.xml b/mage/src/main/res/values/strings.xml index 113f230bd..d8c54ff49 100644 --- a/mage/src/main/res/values/strings.xml +++ b/mage/src/main/res/values/strings.xml @@ -105,19 +105,10 @@ MAGE has been denied access to external storage. To access map caches that reside in external storage please go into the MAGE application settings on your device and enable the Storage permission. Camera or Storage Permission Denied - MAGE has been denied access to the camera or external storage. To create attachments please go into the MAGE application settings on your device and enable the Camera and Storage permissions. + MAGE has been denied access to the camera. To use the camera please go into the MAGE application settings on your device and enable the Camera and Storage permissions. Microphone or Storage Permission Denied - MAGE has been denied access to the microphone or external storage. To create attachments please go into the MAGE application settings on your device and enable the Microphone permission. - - Storage Permission Denied - MAGE has been denied access to external storage. To pick media from the Gallery please go into the MAGE application settings on your device and enable the Storage permission. - - Storage Permission Denied - MAGE has been denied access to external storage. To save media from MAGE please go into the MAGE application settings on your device and enable the Storage permission. - - Storage Permission Denied - MAGE has been denied access to read external storage. Please enable if you would like access to offline layers that reside in external storage. + MAGE has been denied access to the microphone. To create audio files please go into the MAGE application settings on your device and enable the Microphone permission. Location Permission Denied MAGE has been denied access to your location. Please go into the MAGE application settings on your device and enable the Location permission. diff --git a/mage/src/main/res/values/styles.xml b/mage/src/main/res/values/styles.xml index 7d7ac9147..93c5f2d99 100644 --- a/mage/src/main/res/values/styles.xml +++ b/mage/src/main/res/values/styles.xml @@ -122,7 +122,7 @@ normal sans-serif-medium 14sp - ?attr/colorPrimary + @color/md_blue_600