├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── jarRepositories.xml └── migrations.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── it │ │ └── danieleverducci │ │ └── nextcloudmaps │ │ ├── activity │ │ ├── NextcloudMapsStyledActivity.java │ │ ├── about │ │ │ └── AboutActivity.java │ │ ├── detail │ │ │ ├── CategoriesAdapter.java │ │ │ ├── GeofavoriteDetailActivity.java │ │ │ └── GeofavoriteDetailActivityViewModel.java │ │ ├── login │ │ │ └── LoginActivity.java │ │ ├── main │ │ │ ├── GeofavoriteAdapter.java │ │ │ ├── GeofavoritesFragmentViewModel.java │ │ │ ├── MainActivity.java │ │ │ ├── NavigationAdapter.java │ │ │ └── SortingOrderDialogFragment.java │ │ └── mappicker │ │ │ └── MapPickerActivity.java │ │ ├── api │ │ ├── API.java │ │ └── ApiProvider.java │ │ ├── fragments │ │ ├── GeofavoriteListFragment.java │ │ ├── GeofavoriteMapFragment.java │ │ └── GeofavoritesFragment.java │ │ ├── model │ │ └── Geofavorite.java │ │ ├── repository │ │ └── GeofavoriteRepository.java │ │ ├── utils │ │ ├── GeoUriParser.java │ │ ├── GeofavoritesFilter.java │ │ ├── IntentGenerator.java │ │ ├── MapUtils.java │ │ ├── SettingsManager.java │ │ └── SingleLiveEvent.java │ │ └── views │ │ └── GeofavMarkerInfoWindow.java │ ├── res │ ├── drawable │ │ ├── floating_semitransparent_button_background.xml │ │ ├── geofav_infowindow_pointer.xml │ │ ├── ic_accuracy_fail.xml │ │ ├── ic_accuracy_ok.xml │ │ ├── ic_add.xml │ │ ├── ic_add_gps.xml │ │ ├── ic_add_map.xml │ │ ├── ic_alphabetical_asc.xml │ │ ├── ic_app.xml │ │ ├── ic_back_grey.xml │ │ ├── ic_category_asc.xml │ │ ├── ic_clear.xml │ │ ├── ic_copy.xml │ │ ├── ic_delete_grey.xml │ │ ├── ic_distance_asc.xml │ │ ├── ic_edit.xml │ │ ├── ic_filter.xml │ │ ├── ic_filter_off.xml │ │ ├── ic_info_grey.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_list_pin.xml │ │ ├── ic_logout_grey.xml │ │ ├── ic_map_pin.xml │ │ ├── ic_menu_grey.xml │ │ ├── ic_modification_asc.xml │ │ ├── ic_more.xml │ │ ├── ic_nav.xml │ │ ├── ic_ok.xml │ │ ├── ic_save_grey.xml │ │ ├── ic_share.xml │ │ ├── ic_time_grey.xml │ │ ├── ic_view_list.xml │ │ ├── ic_view_map.xml │ │ ├── infowindow_geofav_background.xml │ │ ├── round_button_background.xml │ │ ├── rounded_label_background.xml │ │ ├── unselected_floating_semitransparent_button_background.xml │ │ └── user_badge_mask.xml │ ├── layout │ │ ├── activity_about.xml │ │ ├── activity_geofavorite_detail.xml │ │ ├── activity_list_view.xml │ │ ├── activity_login.xml │ │ ├── activity_main.xml │ │ ├── activity_map_picker.xml │ │ ├── app_toolbar.xml │ │ ├── category_listitem.xml │ │ ├── fragment_geofavorite_list.xml │ │ ├── fragment_geofavorite_map.xml │ │ ├── infowindow_geofav.xml │ │ ├── item_geofav.xml │ │ ├── item_navigation.xml │ │ └── sorting_order_fragment.xml │ ├── menu │ │ └── list_context_menu.xml │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_person.png │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_person.png │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_person.png │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_person.png │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_person.png │ ├── values-it │ │ └── strings.xml │ ├── values-night │ │ └── colors.xml │ ├── values-v23 │ │ └── styles.xml │ ├── values-v27 │ │ └── styles.xml │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── web_hi_res_512.png ├── build.gradle ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── changelogs │ │ ├── 1.txt │ │ ├── 2.txt │ │ ├── 3.txt │ │ ├── 4.txt │ │ ├── 5.txt │ │ ├── 6.txt │ │ ├── 7.txt │ │ └── 9.txt │ ├── full_description.txt │ ├── images │ │ ├── featureGraphic.png │ │ ├── icon.png │ │ ├── phoneScreenshots │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ └── 3.png │ │ └── promoGraphic.png │ ├── short_description.txt │ └── title.txt │ └── it-IT │ ├── full_description.txt │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── 1.png ├── 2.png ├── 3.png └── full │ ├── 1.png │ ├── 2.png │ └── 3.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | # Built application files 16 | *.apk 17 | *.ap_ 18 | *.aab 19 | 20 | # Files for the ART/Dalvik VM 21 | *.dex 22 | 23 | # Java class files 24 | *.class 25 | 26 | # Generated files 27 | bin/ 28 | gen/ 29 | out/ 30 | # Uncomment the following line in case you need and you don't have the release build type files in your app 31 | # release/ 32 | 33 | # Gradle files 34 | .gradle/ 35 | build/ 36 | 37 | # Local configuration file (sdk path, etc) 38 | local.properties 39 | 40 | # Proguard folder generated by Eclipse 41 | proguard/ 42 | 43 | # Log Files 44 | *.log 45 | 46 | # Android Studio Navigation editor temp files 47 | .navigation/ 48 | 49 | # Android Studio captures folder 50 | captures/ 51 | 52 | # IntelliJ 53 | *.iml 54 | .idea/workspace.xml 55 | .idea/tasks.xml 56 | .idea/gradle.xml 57 | .idea/assetWizardSettings.xml 58 | .idea/dictionaries 59 | .idea/libraries 60 | # Android Studio 3 in .gitignore file. 61 | .idea/caches 62 | .idea/modules.xml 63 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 64 | .idea/navEditor.xml 65 | 66 | # Keystore files 67 | # Uncomment the following lines if you do not want to check your keystore files in. 68 | #*.jks 69 | #*.keystore 70 | 71 | # External native build folder generated in Android Studio 2.2 and later 72 | .externalNativeBuild 73 | .cxx/ 74 | 75 | # Google Services (e.g. APIs or Firebase) 76 | # google-services.json 77 | 78 | # Freeline 79 | freeline.py 80 | freeline/ 81 | freeline_project_description.json 82 | 83 | # fastlane 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots 87 | fastlane/test_output 88 | fastlane/readme.md 89 | 90 | # Version control 91 | vcs.xml 92 | 93 | # lint 94 | lint/intermediates/ 95 | lint/generated/ 96 | lint/outputs/ 97 | lint/tmp/ 98 | # lint/reports/ 99 | 100 | app/release/output-metadata.json 101 | 102 | .idea/deploymentTargetDropDown.xml 103 | .idea/misc.xml 104 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Nextcloud Maps Geofavorites -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nextcloud Maps Geofavorites Logo](/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png) 2 | 3 | # Nextcloud Maps Geofavorites Android app 4 | 5 | [Get it on F-Droid](https://f-droid.org/it/packages/it.danieleverducci.nextcloudmaps) 6 | [](https://play.google.com/store/apps/details?id=it.danieleverducci.nextcloudmaps) 7 | [](https://github.com/penguin86/nextcloud-maps-client/releases/latest) 8 | 9 | (Always prefer [F-Droid](https://f-droid.org) build, when possible). 10 | 11 | UNOFFICIAL and FOSS Nextcloud Maps client at its earliest stages of developement. Shows your Nextcloud Maps geofavorites in a list and a map. 12 | Geofavorites can be opened in all apps supporting geo links (i.e. Google Maps, Organic Maps etc...). 13 | A new geofavorite can be created on current location, by sharing a "geo:" uri from another app or manually picking from the map. 14 | 15 | **Requires Maps app to be installed on the Nextcloud instance.** 16 | 17 | This work is heavily based on [matiasdelellis's Nextcloud SSO example](https://github.com/matiasdelellis/app-tutorial-android) to implement [Nextcloud single sign on](https://github.com/nextcloud/Android-SingleSignOn). 18 | 19 | ![Screenshot 1](screenshots/1.png) ![Screenshot 2](screenshots/2.png) ![Screenshot 3](screenshots/3.png) 20 | 21 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Geofavorites for Android 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | apply plugin: 'com.android.application' 19 | 20 | android { 21 | compileSdkVersion 34 22 | 23 | defaultConfig { 24 | applicationId "it.danieleverducci.nextcloudmaps" 25 | minSdkVersion 23 26 | targetSdkVersion 34 27 | versionCode 9 28 | versionName "0.4.0" 29 | 30 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 31 | } 32 | 33 | buildTypes { 34 | release { 35 | minifyEnabled false 36 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 37 | } 38 | } 39 | compileOptions { 40 | // Flag to enable support for the new language APIs 41 | coreLibraryDesugaringEnabled true 42 | // Sets Java compatibility to Java 8 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | 47 | buildFeatures { 48 | dataBinding true 49 | } 50 | namespace 'it.danieleverducci.nextcloudmaps' 51 | } 52 | 53 | repositories { 54 | // Needed for Nextcloud SSO 55 | maven { url "https://jitpack.io" } 56 | } 57 | 58 | dependencies { 59 | implementation fileTree(dir: "libs", include: ["*.jar"]) 60 | 61 | // Desugaring lib: see https://developer.android.com/studio/write/java8-support 62 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") 63 | 64 | implementation 'com.android.support:design:34.0.0' 65 | implementation 'androidx.appcompat:appcompat:1.6.1' 66 | implementation 'androidx.recyclerview:recyclerview:1.3.2' 67 | implementation "androidx.cardview:cardview:1.0.0" 68 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 69 | implementation "androidx.preference:preference:1.2.1" 70 | implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 71 | 72 | // Retrofif2 73 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 74 | implementation 'com.squareup.retrofit2:converter-gson:2.6.1' 75 | 76 | // Nextcloud SSO 77 | implementation "com.github.nextcloud:Android-SingleSignOn:1.0.0" 78 | 79 | // OSMDroid 80 | implementation 'org.osmdroid:osmdroid-android:6.1.18' 81 | 82 | //Threeten-Backport (ports Java 8 Date API on Java 6+) 83 | implementation 'org.threeten:threetenbp:1.5.1' 84 | 85 | // https://mvnrepository.com/artifact/commons-io/commons-io 86 | implementation 'commons-io:commons-io:2.11.0' 87 | 88 | // Picasso (image loader) 89 | implementation 'com.squareup.picasso:picasso:2.8' 90 | 91 | configurations.all { 92 | resolutionStrategy { 93 | force 'commons-io:commons-io:2.11.0' 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 109 | 110 | 111 | 112 | 113 | 114 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguin86/nextcloud-maps-client/bd8a0d706dc7bbb34c8b958dc1a24d0e8897bd33/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/NextcloudMapsStyledActivity.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.activity; 2 | 3 | import android.content.res.Configuration; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | 7 | import androidx.annotation.Nullable; 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | import org.osmdroid.views.overlay.TilesOverlay; 11 | 12 | public class NextcloudMapsStyledActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(@Nullable Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | 18 | // For whatever reason, android:windowLightStatusBar is ignored in styles.xml 19 | int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 20 | if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) 21 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); 22 | } 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/about/AboutActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Geofavorites for Android 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package it.danieleverducci.nextcloudmaps.activity.about; 19 | 20 | import android.content.Intent; 21 | import android.net.Uri; 22 | import android.os.Bundle; 23 | import android.text.Html; 24 | import android.widget.Button; 25 | import android.widget.TextView; 26 | 27 | import androidx.appcompat.app.ActionBar; 28 | import androidx.appcompat.app.AppCompatActivity; 29 | import androidx.appcompat.widget.Toolbar; 30 | 31 | import it.danieleverducci.nextcloudmaps.BuildConfig; 32 | import it.danieleverducci.nextcloudmaps.R; 33 | import it.danieleverducci.nextcloudmaps.activity.NextcloudMapsStyledActivity; 34 | 35 | public class AboutActivity extends NextcloudMapsStyledActivity { 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_about); 40 | 41 | Toolbar toolbar = findViewById(R.id.toolbar); 42 | setSupportActionBar(toolbar); 43 | 44 | ActionBar actionBar = getSupportActionBar(); 45 | if (actionBar != null) { 46 | actionBar.setDisplayShowTitleEnabled(false); 47 | } 48 | 49 | fillAboutActivity(); 50 | } 51 | 52 | private void fillAboutActivity () { 53 | TextView tvVersion = findViewById(R.id.about_version); 54 | tvVersion.setText(Html.fromHtml(getString(R.string.about_version, "v" + BuildConfig.VERSION_NAME))); 55 | 56 | Button btLicence = findViewById(R.id.about_app_license_button); 57 | btLicence.setOnClickListener(view -> openUtl(getString(R.string.url_license))); 58 | 59 | TextView tvSource = findViewById(R.id.about_source); 60 | tvSource.setText(Html.fromHtml(getString(R.string.about_source, getString(R.string.url_source)))); 61 | tvSource.setOnClickListener(view -> openUtl(getString(R.string.url_source))); 62 | 63 | TextView tvIssues = findViewById(R.id.about_issues); 64 | tvIssues.setText(Html.fromHtml(getString(R.string.about_issues, getString(R.string.url_issues)))); 65 | tvIssues.setOnClickListener(view -> openUtl(getString(R.string.url_issues))); 66 | 67 | TextView tvMaps = findViewById(R.id.about_maps); 68 | tvMaps.setText(Html.fromHtml(getString(R.string.about_maps))); 69 | tvMaps.setOnClickListener(view -> openUtl(getString(R.string.url_maps))); 70 | } 71 | 72 | @Override 73 | public boolean onSupportNavigateUp() { 74 | finish(); 75 | return true; 76 | } 77 | 78 | private void openUtl(String url) { 79 | startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/detail/CategoriesAdapter.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.activity.detail; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.TextView; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.core.graphics.drawable.DrawableCompat; 13 | 14 | import java.util.ArrayList; 15 | import java.util.HashSet; 16 | 17 | import it.danieleverducci.nextcloudmaps.R; 18 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 19 | 20 | public class CategoriesAdapter extends ArrayAdapter { 21 | 22 | public CategoriesAdapter(@NonNull Context context) { 23 | super(context, R.layout.category_listitem, R.id.category_name, new ArrayList<>()); 24 | } 25 | 26 | @NonNull 27 | @Override 28 | public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { 29 | View v = super.getView(position, convertView, parent); 30 | TextView categoryName = v.findViewById(R.id.category_name); 31 | Drawable backgroundDrawable = categoryName.getBackground(); 32 | DrawableCompat.setTint( 33 | backgroundDrawable, 34 | Geofavorite.categoryColorFromName(categoryName.getText().toString()) == 0 35 | ? v.getContext().getColor(R.color.defaultBrand) 36 | : Geofavorite.categoryColorFromName(categoryName.getText().toString()) 37 | ); 38 | return v; 39 | } 40 | 41 | public void setCategoriesList(HashSet categories) { 42 | clear(); 43 | addAll(categories); 44 | notifyDataSetChanged(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/detail/GeofavoriteDetailActivityViewModel.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.activity.detail; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.lifecycle.LiveData; 6 | import androidx.lifecycle.MutableLiveData; 7 | import androidx.lifecycle.ViewModel; 8 | 9 | import java.util.HashMap; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | 13 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 14 | import it.danieleverducci.nextcloudmaps.repository.GeofavoriteRepository; 15 | 16 | public class GeofavoriteDetailActivityViewModel extends ViewModel { 17 | private GeofavoriteRepository mRepo; 18 | 19 | public void init(Context applicationContext) { 20 | mRepo = GeofavoriteRepository.getInstance(applicationContext); 21 | } 22 | 23 | public Geofavorite getGeofavorite(int id) { 24 | return mRepo.getGeofavorite(id); 25 | } 26 | 27 | public void saveGeofavorite(Geofavorite geofav) { 28 | mRepo.saveGeofavorite(geofav); 29 | } 30 | 31 | public LiveData> getCategories(){ 32 | return mRepo.getCategories(); 33 | } 34 | 35 | public LiveData getIsUpdating(){ 36 | return mRepo.isUpdating(); 37 | } 38 | 39 | public LiveData getOnFinished(){ 40 | return mRepo.onFinished(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/login/LoginActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Geofavorites for Android 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package it.danieleverducci.nextcloudmaps.activity.login; 19 | 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.os.Bundle; 23 | import android.os.Handler; 24 | import android.util.Log; 25 | import android.view.View; 26 | import android.widget.Button; 27 | import android.widget.ProgressBar; 28 | 29 | import androidx.annotation.NonNull; 30 | import androidx.appcompat.app.AppCompatActivity; 31 | 32 | import com.nextcloud.android.sso.AccountImporter; 33 | import com.nextcloud.android.sso.api.NextcloudAPI; 34 | import com.nextcloud.android.sso.exceptions.AccountImportCancelledException; 35 | import com.nextcloud.android.sso.exceptions.AndroidGetAccountsPermissionNotGranted; 36 | import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; 37 | import com.nextcloud.android.sso.exceptions.NextcloudFilesAppNotInstalledException; 38 | import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; 39 | import com.nextcloud.android.sso.helper.SingleAccountHelper; 40 | import com.nextcloud.android.sso.model.SingleSignOnAccount; 41 | import com.nextcloud.android.sso.ui.UiExceptionManager; 42 | 43 | import it.danieleverducci.nextcloudmaps.R; 44 | import it.danieleverducci.nextcloudmaps.activity.NextcloudMapsStyledActivity; 45 | import it.danieleverducci.nextcloudmaps.activity.main.MainActivity; 46 | import it.danieleverducci.nextcloudmaps.api.API; 47 | import it.danieleverducci.nextcloudmaps.api.ApiProvider; 48 | 49 | public class LoginActivity extends NextcloudMapsStyledActivity { 50 | private static final String TAG = "LoginActivity"; 51 | 52 | protected ProgressBar progress; 53 | protected Button button; 54 | 55 | protected SingleSignOnAccount ssoAccount; 56 | 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | setContentView(R.layout.activity_login); 61 | 62 | progress = findViewById(R.id.progress); 63 | button = findViewById(R.id.chose_button); 64 | button.setOnClickListener(view -> { 65 | progress.setVisibility(View.VISIBLE); 66 | openAccountChooser(); 67 | }); 68 | 69 | Handler h = new Handler(); 70 | h.post(() -> { 71 | try { 72 | ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()); 73 | SingleAccountHelper.applyCurrentAccount(getApplicationContext(), ssoAccount.name); 74 | accountAccessDone(); 75 | } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { 76 | Log.e(TAG, "Autologin: " + e.toString()); 77 | } 78 | }); 79 | } 80 | private void openAccountChooser() { 81 | try { 82 | AccountImporter.pickNewAccount(this); 83 | } catch (NextcloudFilesAppNotInstalledException | AndroidGetAccountsPermissionNotGranted e) { 84 | UiExceptionManager.showDialogForException(this, e); 85 | 86 | Log.e(TAG, "openAccountChooser: " + e.toString()); 87 | progress.setVisibility(View.GONE); 88 | } 89 | } 90 | 91 | public void onActivityResult(int requestCode, int resultCode, Intent data) { 92 | super.onActivityResult(requestCode, resultCode, data); 93 | 94 | try { 95 | AccountImporter.onActivityResult(requestCode, resultCode, data, this, new AccountImporter.IAccountAccessGranted() { 96 | NextcloudAPI.ApiConnectedListener callback = new NextcloudAPI.ApiConnectedListener() { 97 | @Override 98 | public void onConnected() { 99 | // ignore this one… see 5) 100 | } 101 | 102 | @Override 103 | public void onError(Exception ex) { 104 | // TODO handle errors 105 | } 106 | }; 107 | 108 | @Override 109 | public void accountAccessGranted(SingleSignOnAccount account) { 110 | Context l_context = getApplicationContext(); 111 | SingleAccountHelper.applyCurrentAccount(l_context, account.name); 112 | 113 | accountAccessDone(); 114 | } 115 | }); 116 | } catch (AccountImportCancelledException e) { 117 | e.printStackTrace(); 118 | } 119 | } 120 | 121 | @Override 122 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 123 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 124 | AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this); 125 | } 126 | 127 | private void accountAccessDone() { 128 | Intent intent = new Intent(LoginActivity.this, MainActivity.class); 129 | startActivity(intent); 130 | 131 | finish(); 132 | } 133 | } -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/main/GeofavoritesFragmentViewModel.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.activity.main; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.lifecycle.LiveData; 6 | import androidx.lifecycle.ViewModel; 7 | 8 | import java.util.HashSet; 9 | import java.util.List; 10 | 11 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 12 | import it.danieleverducci.nextcloudmaps.repository.GeofavoriteRepository; 13 | 14 | public class GeofavoritesFragmentViewModel extends ViewModel { 15 | private GeofavoriteRepository mRepo; 16 | 17 | public void init(Context applicationContext) { 18 | mRepo = GeofavoriteRepository.getInstance(applicationContext); 19 | } 20 | 21 | public LiveData> getGeofavorites(){ 22 | mRepo.updateGeofavorites(); 23 | return mRepo.getGeofavorites(); 24 | } 25 | 26 | public void updateGeofavorites() { 27 | mRepo.updateGeofavorites(); 28 | } 29 | 30 | public LiveData> getCategories(){ 31 | return mRepo.getCategories(); 32 | } 33 | 34 | public void deleteGeofavorite(Geofavorite geofav) { 35 | mRepo.deleteGeofavorite(geofav); 36 | } 37 | 38 | public LiveData getIsUpdating(){ 39 | return mRepo.isUpdating(); 40 | } 41 | 42 | public LiveData getOnFinished(){ 43 | return mRepo.onFinished(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/main/NavigationAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Maps Geofavorites for Android 3 | * 4 | * @copyright Copyright (c) 2020 John Doe 5 | * @author John Doe 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package it.danieleverducci.nextcloudmaps.activity.main; 22 | 23 | import android.content.Context; 24 | import android.view.LayoutInflater; 25 | import android.view.View; 26 | import android.view.ViewGroup; 27 | import android.widget.ImageView; 28 | import android.widget.TextView; 29 | 30 | import androidx.annotation.DrawableRes; 31 | import androidx.annotation.NonNull; 32 | import androidx.core.graphics.drawable.DrawableCompat; 33 | import androidx.recyclerview.widget.RecyclerView; 34 | 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | 38 | import it.danieleverducci.nextcloudmaps.R; 39 | 40 | public class NavigationAdapter extends RecyclerView.Adapter { 41 | 42 | @NonNull 43 | private final Context context; 44 | 45 | public static class NavigationItem { 46 | @NonNull 47 | public String id; 48 | @NonNull 49 | public String label; 50 | @DrawableRes 51 | public int icon; 52 | 53 | public NavigationItem(@NonNull String id, @NonNull String label, @DrawableRes int icon) { 54 | this.id = id; 55 | this.label = label; 56 | this.icon = icon; 57 | } 58 | } 59 | 60 | class ViewHolder extends RecyclerView.ViewHolder { 61 | @NonNull 62 | private final View view; 63 | 64 | @NonNull 65 | private final TextView name; 66 | @NonNull 67 | private final ImageView icon; 68 | 69 | private NavigationItem currentItem; 70 | 71 | ViewHolder(@NonNull View itemView, @NonNull final ClickListener clickListener) { 72 | super(itemView); 73 | view = itemView; 74 | 75 | this.name = itemView.findViewById(R.id.navigationItemLabel); 76 | this.icon = itemView.findViewById(R.id.navigationItemIcon); 77 | 78 | icon.setOnClickListener(view -> clickListener.onItemClick(currentItem)); 79 | itemView.setOnClickListener(view -> clickListener.onItemClick(currentItem)); 80 | } 81 | 82 | private void bind(@NonNull NavigationItem item) { 83 | 84 | currentItem = item; 85 | name.setText(item.label); 86 | 87 | 88 | icon.setImageDrawable(DrawableCompat.wrap(icon.getResources().getDrawable(item.icon))); 89 | icon.setVisibility(View.VISIBLE); 90 | } 91 | 92 | } 93 | 94 | public interface ClickListener { 95 | void onItemClick(NavigationItem item); 96 | } 97 | 98 | @NonNull 99 | private List items = new ArrayList<>(); 100 | private String selectedItem = null; 101 | @NonNull 102 | private final ClickListener clickListener; 103 | 104 | public NavigationAdapter(@NonNull Context context, @NonNull ClickListener clickListener) { 105 | this.context = context; 106 | this.clickListener = clickListener; 107 | } 108 | 109 | @NonNull 110 | @Override 111 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 112 | View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_navigation, parent, false); 113 | return new ViewHolder(v, clickListener); 114 | } 115 | 116 | @Override 117 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 118 | holder.bind(items.get(position)); 119 | } 120 | 121 | @Override 122 | public int getItemCount() { 123 | return items.size(); 124 | } 125 | 126 | public void setItems(@NonNull List items) { 127 | this.items = items; 128 | notifyDataSetChanged(); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/activity/main/SortingOrderDialogFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Maps Geofavorites for Android 3 | * 4 | * @copyright Copyright (c) 2020 John Doe 5 | * @author John Doe 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package it.danieleverducci.nextcloudmaps.activity.main; 22 | 23 | import android.app.Dialog; 24 | import android.graphics.Typeface; 25 | import android.graphics.drawable.Drawable; 26 | import android.os.Bundle; 27 | import android.view.LayoutInflater; 28 | import android.view.View; 29 | import android.view.ViewGroup; 30 | import android.widget.Button; 31 | import android.widget.ImageButton; 32 | import android.widget.TextView; 33 | 34 | import androidx.annotation.NonNull; 35 | import androidx.core.content.ContextCompat; 36 | import androidx.core.graphics.drawable.DrawableCompat; 37 | import androidx.fragment.app.DialogFragment; 38 | 39 | import it.danieleverducci.nextcloudmaps.R; 40 | 41 | /** 42 | * Dialog to show and choose the sorting order for the file listing. 43 | */ 44 | public class SortingOrderDialogFragment extends DialogFragment { 45 | 46 | private final static String TAG = SortingOrderDialogFragment.class.getSimpleName(); 47 | 48 | public static final String SORTING_ORDER_FRAGMENT = "SORTING_ORDER_FRAGMENT"; 49 | private static final String KEY_SORT_ORDER = "SORT_ORDER"; 50 | 51 | private OnSortingOrderListener onSortingOrderListener; 52 | private View mView; 53 | private View[] mTaggedViews; 54 | private Button mCancel; 55 | 56 | private int mCurrentSortOrder; 57 | 58 | public static SortingOrderDialogFragment newInstance(int sortOrder) { 59 | SortingOrderDialogFragment dialogFragment = new SortingOrderDialogFragment(); 60 | 61 | Bundle args = new Bundle(); 62 | args.putInt(KEY_SORT_ORDER, sortOrder); 63 | dialogFragment.setArguments(args); 64 | 65 | return dialogFragment; 66 | } 67 | 68 | @Override 69 | public void onCreate(Bundle savedInstanceState) { 70 | super.onCreate(savedInstanceState); 71 | 72 | // keep the state of the fragment on configuration changes 73 | setRetainInstance(true); 74 | 75 | mView = null; 76 | 77 | Bundle arguments = getArguments(); 78 | if (arguments == null) { 79 | throw new IllegalArgumentException("Arguments may not be null"); 80 | } 81 | mCurrentSortOrder = arguments.getInt(KEY_SORT_ORDER, GeofavoriteAdapter.SORT_BY_TITLE); 82 | } 83 | 84 | @Override 85 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 86 | mView = inflater.inflate(R.layout.sorting_order_fragment, container, false); 87 | 88 | setupDialogElements(mView); 89 | setupListeners(); 90 | 91 | return mView; 92 | } 93 | 94 | public void setOnSortingOrderListener(OnSortingOrderListener listener) { 95 | this.onSortingOrderListener = listener; 96 | } 97 | 98 | /** 99 | * find all relevant UI elements and set their values. 100 | * TODO: this is REALLY ugly. 101 | * 102 | * @param view the parent view 103 | */ 104 | private void setupDialogElements(View view) { 105 | mCancel = view.findViewById(R.id.cancel); 106 | 107 | mTaggedViews = new View[8]; 108 | mTaggedViews[0] = view.findViewById(R.id.sortByTitleAscending); 109 | mTaggedViews[0].setTag(GeofavoriteAdapter.SORT_BY_TITLE); 110 | mTaggedViews[1] = view.findViewById(R.id.sortByTitleAscendingText); 111 | mTaggedViews[1].setTag(GeofavoriteAdapter.SORT_BY_TITLE); 112 | mTaggedViews[2] = view.findViewById(R.id.sortByCreationDateDescending); 113 | mTaggedViews[2].setTag(GeofavoriteAdapter.SORT_BY_CREATED); 114 | mTaggedViews[3] = view.findViewById(R.id.sortByCreationDateDescendingText); 115 | mTaggedViews[3].setTag(GeofavoriteAdapter.SORT_BY_CREATED); 116 | mTaggedViews[4] = view.findViewById(R.id.sortByCategoryAscending); 117 | mTaggedViews[4].setTag(GeofavoriteAdapter.SORT_BY_CATEGORY); 118 | mTaggedViews[5] = view.findViewById(R.id.sortByCategoryAscendingText); 119 | mTaggedViews[5].setTag(GeofavoriteAdapter.SORT_BY_CATEGORY); 120 | mTaggedViews[6] = view.findViewById(R.id.sortByDistanceAscending); 121 | mTaggedViews[6].setTag(GeofavoriteAdapter.SORT_BY_DISTANCE); 122 | mTaggedViews[7] = view.findViewById(R.id.sortByDistanceAscendingText); 123 | mTaggedViews[7].setTag(GeofavoriteAdapter.SORT_BY_DISTANCE); 124 | 125 | setupActiveOrderSelection(); 126 | } 127 | 128 | /** 129 | * tints the icon reflecting the actual sorting choice in the apps primary color. 130 | */ 131 | private void setupActiveOrderSelection() { 132 | for (View view: mTaggedViews) { 133 | if (mCurrentSortOrder != (int) view.getTag()) { 134 | continue; 135 | } 136 | if (view instanceof ImageButton) { 137 | Drawable normalDrawable = ((ImageButton) view).getDrawable(); 138 | Drawable wrapDrawable = DrawableCompat.wrap(normalDrawable); 139 | DrawableCompat.setTint(wrapDrawable, ContextCompat.getColor(getContext(), R.color.selector_item_selected)); 140 | } 141 | if (view instanceof TextView) { 142 | ((TextView)view).setTextColor(ContextCompat.getColor(getContext(), R.color.selector_item_selected)); 143 | ((TextView)view).setTypeface(Typeface.DEFAULT_BOLD); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * setup all listeners. 150 | */ 151 | private void setupListeners() { 152 | mCancel.setOnClickListener(view -> dismiss()); 153 | 154 | OnSortOrderClickListener sortOrderClickListener = new OnSortOrderClickListener(); 155 | 156 | for (View view : mTaggedViews) { 157 | view.setOnClickListener(sortOrderClickListener); 158 | } 159 | } 160 | 161 | @Override 162 | @NonNull 163 | public Dialog onCreateDialog(Bundle savedInstanceState) { 164 | return super.onCreateDialog(savedInstanceState); 165 | } 166 | 167 | @Override 168 | public void onDestroyView() { 169 | if (getDialog() != null && getRetainInstance()) { 170 | getDialog().setDismissMessage(null); 171 | } 172 | super.onDestroyView(); 173 | } 174 | 175 | private class OnSortOrderClickListener implements View.OnClickListener { 176 | @Override 177 | public void onClick(View v) { 178 | dismissAllowingStateLoss(); 179 | if (onSortingOrderListener != null) 180 | onSortingOrderListener.onSortingOrderChosen((int) v.getTag()); 181 | } 182 | } 183 | 184 | public interface OnSortingOrderListener { 185 | void onSortingOrderChosen(int sortSelection); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/api/API.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Maps Geofavorites for Android 3 | * 4 | * @copyright Copyright (c) 2020 John Doe 5 | * @author John Doe 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package it.danieleverducci.nextcloudmaps.api; 22 | 23 | import java.util.List; 24 | 25 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 26 | import retrofit2.Call; 27 | import retrofit2.http.Body; 28 | import retrofit2.http.DELETE; 29 | import retrofit2.http.GET; 30 | import retrofit2.http.POST; 31 | import retrofit2.http.PUT; 32 | import retrofit2.http.Path; 33 | 34 | public interface API { 35 | String mApiEndpoint = "/index.php/apps/maps/api/1.0"; 36 | 37 | @GET("/favorites") 38 | Call> getGeofavorites(); 39 | 40 | @POST("/favorites") 41 | Call createGeofavorite ( 42 | @Body Geofavorite geofavorite 43 | ); 44 | 45 | @PUT("/favorites/{id}") 46 | Call updateGeofavorite ( 47 | @Path("id") int id, 48 | @Body Geofavorite geofavorite 49 | ); 50 | 51 | @DELETE("/favorites/{id}") 52 | Call deleteGeofavorite ( 53 | @Path("id") int id 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/api/ApiProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Maps Geofavorites for Android 3 | * 4 | * @copyright Copyright (c) 2020 John Doe 5 | * @author John Doe 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package it.danieleverducci.nextcloudmaps.api; 22 | 23 | import android.content.Context; 24 | import android.util.Log; 25 | 26 | import androidx.annotation.NonNull; 27 | import androidx.annotation.Nullable; 28 | 29 | import com.google.gson.GsonBuilder; 30 | import com.nextcloud.android.sso.api.NextcloudAPI; 31 | import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; 32 | import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException; 33 | import com.nextcloud.android.sso.helper.SingleAccountHelper; 34 | import com.nextcloud.android.sso.model.SingleSignOnAccount; 35 | 36 | import retrofit2.NextcloudRetrofitApiBuilder; 37 | 38 | public class ApiProvider { 39 | private static final String TAG = ApiProvider.class.getCanonicalName(); 40 | 41 | protected static API mApi; 42 | 43 | @Nullable 44 | public static API getAPI(Context context) { 45 | if (mApi == null) { 46 | try { 47 | SingleSignOnAccount ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context); 48 | NextcloudAPI nextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create()); 49 | 50 | mApi = new NextcloudRetrofitApiBuilder(nextcloudAPI, API.mApiEndpoint).create(API.class); 51 | } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) { 52 | Log.d(TAG, "setAccout() called with: ex = [" + e + "]"); 53 | } 54 | } 55 | 56 | return mApi; 57 | } 58 | 59 | public static void logout() { 60 | mApi = null; 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/fragments/GeofavoriteListFragment.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.fragments; 2 | 3 | import static it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter.SORT_BY_CATEGORY; 4 | import static it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter.SORT_BY_CREATED; 5 | import static it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter.SORT_BY_DISTANCE; 6 | import static it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter.SORT_BY_TITLE; 7 | 8 | import android.os.Bundle; 9 | import android.view.LayoutInflater; 10 | import android.view.View; 11 | import android.view.ViewGroup; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.appcompat.widget.AppCompatImageView; 16 | import androidx.fragment.app.FragmentTransaction; 17 | import androidx.lifecycle.Observer; 18 | import androidx.recyclerview.widget.LinearLayoutManager; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | import it.danieleverducci.nextcloudmaps.R; 26 | import it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter; 27 | import it.danieleverducci.nextcloudmaps.activity.main.MainActivity; 28 | import it.danieleverducci.nextcloudmaps.activity.main.SortingOrderDialogFragment; 29 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 30 | import it.danieleverducci.nextcloudmaps.utils.GeofavoritesFilter; 31 | import it.danieleverducci.nextcloudmaps.utils.SettingsManager; 32 | 33 | public class GeofavoriteListFragment extends GeofavoritesFragment implements SortingOrderDialogFragment.OnSortingOrderListener { 34 | 35 | private SwipeRefreshLayout swipeRefresh; 36 | private GeofavoriteAdapter geofavoriteAdapter; 37 | 38 | @Nullable 39 | @Override 40 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 41 | View v = inflater.inflate(R.layout.fragment_geofavorite_list, container, false); 42 | 43 | // Setup list 44 | int sortRule = SettingsManager.getGeofavoriteListSortBy(requireContext()); 45 | 46 | RecyclerView recyclerView = v.findViewById(R.id.recycler_view); 47 | LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext()); 48 | recyclerView.setLayoutManager(layoutManager); 49 | 50 | GeofavoriteAdapter.ItemClickListener rvItemClickListener = new GeofavoriteAdapter.ItemClickListener() { 51 | @Override 52 | public void onItemClick(Geofavorite item) { 53 | openGeofavorite(item); 54 | } 55 | 56 | @Override 57 | public void onItemShareClick(Geofavorite item) { 58 | shareGeofavorite(item); 59 | } 60 | 61 | @Override 62 | public void onItemNavClick(Geofavorite item) { 63 | navigateToGeofavorite(item); 64 | } 65 | 66 | @Override 67 | public void onItemDeleteClick(Geofavorite item) { 68 | deleteGeofavorite(item); 69 | } 70 | }; 71 | 72 | geofavoriteAdapter = new GeofavoriteAdapter(requireContext(), rvItemClickListener); 73 | recyclerView.setAdapter(geofavoriteAdapter); 74 | geofavoriteAdapter.setSortRule(sortRule); 75 | 76 | // Register for data source events 77 | mGeofavoritesFragmentViewModel.getIsUpdating().observe(getViewLifecycleOwner(), new Observer() { 78 | @Override 79 | public void onChanged(@Nullable Boolean changed) { 80 | if(Boolean.TRUE.equals(changed)){ 81 | swipeRefresh.setRefreshing(true); 82 | } 83 | else{ 84 | swipeRefresh.setRefreshing(false); 85 | } 86 | } 87 | }); 88 | 89 | // Setup view listeners 90 | swipeRefresh = v.findViewById(R.id.swipe_refresh); 91 | swipeRefresh.setOnRefreshListener(() -> 92 | mGeofavoritesFragmentViewModel.updateGeofavorites()); 93 | 94 | AppCompatImageView sortButton = v.findViewById(R.id.sort_mode); 95 | sortButton.setOnClickListener(view -> openSortingOrderDialogFragment(geofavoriteAdapter.getSortRule())); 96 | 97 | View showMapButton = v.findViewById(R.id.view_mode_map); 98 | showMapButton.setOnClickListener(View -> ((MainActivity)requireActivity()).showMap()); 99 | 100 | return v; 101 | } 102 | 103 | @Override 104 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 105 | super.onViewCreated(view, savedInstanceState); 106 | 107 | // Set icons 108 | int sortRule = SettingsManager.getGeofavoriteListSortBy(requireContext()); 109 | updateSortingIcon(sortRule); 110 | } 111 | 112 | @Override 113 | public void onDatasetChange(List items) { 114 | // Called when the items are loaded or a filtering happens 115 | geofavoriteAdapter.setGeofavoriteList(items); 116 | } 117 | 118 | @Override 119 | public void onSortingOrderChosen(int sortSelection) { 120 | geofavoriteAdapter.setSortRule(sortSelection); 121 | updateSortingIcon(sortSelection); 122 | 123 | SettingsManager.setGeofavoriteListSortBy(requireContext(), sortSelection); 124 | } 125 | 126 | private void openSortingOrderDialogFragment(int sortOrder) { 127 | FragmentTransaction fragmentTransaction = requireActivity().getSupportFragmentManager().beginTransaction(); 128 | fragmentTransaction.addToBackStack(null); 129 | 130 | SortingOrderDialogFragment sodf = SortingOrderDialogFragment.newInstance(sortOrder); 131 | sodf.setOnSortingOrderListener(this); 132 | sodf.show(fragmentTransaction, SortingOrderDialogFragment.SORTING_ORDER_FRAGMENT); 133 | } 134 | 135 | private void updateSortingIcon(int sortSelection) { 136 | AppCompatImageView sortButton = requireView().findViewById(R.id.sort_mode); 137 | switch (sortSelection) { 138 | case SORT_BY_TITLE: 139 | sortButton.setImageResource(R.drawable.ic_alphabetical_asc); 140 | break; 141 | case SORT_BY_CREATED: 142 | sortButton.setImageResource(R.drawable.ic_modification_asc); 143 | break; 144 | case SORT_BY_CATEGORY: 145 | sortButton.setImageResource(R.drawable.ic_category_asc); 146 | break; 147 | case SORT_BY_DISTANCE: 148 | sortButton.setImageResource(R.drawable.ic_distance_asc); 149 | break; 150 | } 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/model/Geofavorite.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Nextcloud Maps Geofavorites for Android 3 | * 4 | * @copyright Copyright (c) 2020 John Doe 5 | * @author John Doe 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | package it.danieleverducci.nextcloudmaps.model; 22 | 23 | import android.graphics.Color; 24 | import android.net.Uri; 25 | import android.widget.Filter; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | 30 | import com.google.gson.annotations.Expose; 31 | import com.google.gson.annotations.SerializedName; 32 | 33 | import org.threeten.bp.Instant; 34 | import org.threeten.bp.LocalDate; 35 | import org.threeten.bp.ZoneId; 36 | 37 | import java.io.Serializable; 38 | import java.util.ArrayList; 39 | import java.util.Comparator; 40 | import java.util.List; 41 | 42 | import it.danieleverducci.nextcloudmaps.utils.GeoUriParser; 43 | 44 | public class Geofavorite implements Serializable { 45 | public static final String DEFAULT_CATEGORY = "Personal"; 46 | private static final double EARTH_RADIUS = 6371; // https://en.wikipedia.org/wiki/Earth_radius 47 | 48 | /** 49 | * JSON Definition: 50 | * { 51 | * "id": 20, 52 | * "name": "Ipermercato Collestrada", 53 | * "date_modified": 1626798839, 54 | * "date_created": 1626798825, 55 | * "lat": 43.08620320282127, 56 | * "lng": 12.481070617773184, 57 | * "category": "Personal", 58 | * "comment": "Strada Centrale Umbra 06135 Collestrada Umbria Italia", 59 | * "extensions": "" 60 | * } 61 | */ 62 | 63 | @Expose 64 | @SerializedName("id") private int id; 65 | 66 | @Expose 67 | @Nullable 68 | @SerializedName("name") private String name; 69 | 70 | @Expose 71 | @SerializedName("date_modified") private long dateModified; 72 | 73 | @Expose 74 | @SerializedName("date_created") private long dateCreated; 75 | 76 | @Expose 77 | @SerializedName("lat") private double lat; 78 | 79 | @Expose 80 | @SerializedName("lng") private double lng; 81 | 82 | @Expose 83 | @SerializedName("category") private String category; 84 | 85 | @Expose 86 | @Nullable 87 | @SerializedName("comment") private String comment; 88 | 89 | public int getId() { 90 | return id; 91 | } 92 | 93 | public void setId(int id) { 94 | this.id = id; 95 | } 96 | 97 | @Nullable 98 | public String getName() { 99 | return name; 100 | } 101 | 102 | public void setName(String name) { 103 | if (!name.equals(this.name)) 104 | this.name = name; 105 | } 106 | 107 | public long getDateModified() { 108 | return dateModified; 109 | } 110 | public void setDateModified(long dateModified) { 111 | this.dateModified = dateModified; 112 | } 113 | public LocalDate getLocalDateModified() { 114 | return Instant.ofEpochSecond(getDateCreated()).atZone(ZoneId.systemDefault()).toLocalDate(); 115 | } 116 | 117 | public long getDateCreated() { 118 | return dateCreated; 119 | } 120 | public LocalDate getLocalDateCreated() { 121 | return Instant.ofEpochSecond(getDateCreated()).atZone(ZoneId.systemDefault()).toLocalDate(); 122 | } 123 | 124 | public void setDateCreated(long dateCreated) { 125 | this.dateCreated = dateCreated; 126 | } 127 | 128 | public double getLat() { 129 | return lat; 130 | } 131 | public void setLat(double lat) { 132 | this.lat = lat; 133 | } 134 | 135 | public double getLng() { 136 | return lng; 137 | } 138 | public void setLng(double lng) { 139 | this.lng = lng; 140 | } 141 | 142 | public String getCategory() { 143 | return category; 144 | } 145 | public void setCategory(String category) { 146 | this.category = category; 147 | } 148 | 149 | @Nullable 150 | public String getComment() { 151 | return comment; 152 | } 153 | public void setComment(String comment) { 154 | this.comment = comment; 155 | } 156 | 157 | /** 158 | * Comparators for list order 159 | */ 160 | public static Comparator ByTitleAZ = (gf0, gf1) -> gf0.name.compareTo(gf1.name); 161 | public static Comparator ByLastCreated = (gf0, gf1) -> (int) (gf1.dateCreated - gf0.dateCreated); 162 | public static Comparator ByCategory = (gf0, gf1) -> (gf0.category + gf0.name).compareTo(gf1.category + gf1.name); 163 | public static Comparator ByDistance = (gf0, gf1) -> 0; // (int) ((gf1.getDistanceFrom(userPosition) - gf0.getDistanceFrom(userPosition)) * 1000); 164 | 165 | public String getCoordinatesString() { 166 | return this.lat + " N, " + this.lng + " E"; 167 | } 168 | 169 | public Uri getGeoUri() { 170 | return GeoUriParser.createGeoUri(this.lat, this.lng, this.name); 171 | } 172 | public Uri getGmapsUri() { 173 | return GeoUriParser.createGmapsUri(this.lat, this.lng); 174 | } 175 | 176 | public boolean valid() { 177 | return 178 | getLat() != 0 && getLng() != 0 && 179 | getName() != null && getName().length() > 0 && 180 | getCategory() != null && getCategory().length() > 0; 181 | } 182 | 183 | /** 184 | * Returns the distance between the current Geofavorite and the provided one, in kilometers. 185 | * @param other Geovavorite 186 | * @return the distance in kilometers 187 | */ 188 | public double getDistanceFrom(Geofavorite other) { 189 | double latDistance = (other.lat-lat) * Math.PI / 180; 190 | double lonDistance = (other.lng-lng) * Math.PI / 180; 191 | double a = 192 | Math.sin(latDistance / 2) * Math.sin(latDistance / 2) + 193 | Math.cos(lat * Math.PI / 180) * Math.cos(other.lat * Math.PI / 180) * 194 | Math.sin(lonDistance / 2) * Math.sin(lonDistance / 2); 195 | double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 196 | return EARTH_RADIUS * c; 197 | } 198 | 199 | /** 200 | * Based on Nextcloud Maps's getLetterColor util. 201 | * Assigns a color to a category based on its two first letters. 202 | * 203 | * @see "https://github.com/nextcloud/maps/blob/master/src/utils.js" 204 | * @return the generated color or null for the default category 205 | */ 206 | public int categoryColor() { 207 | return categoryColorFromName(this.category); 208 | } 209 | 210 | public String categoryLetter() { 211 | if (category == null || category.length() == 0 || category.equals(DEFAULT_CATEGORY)) 212 | return "\u2022"; 213 | return category.substring(0,1); 214 | } 215 | 216 | @NonNull 217 | @Override 218 | public String toString() { 219 | return "[" + getName() + " (" + getLat() + "," + getLng() + ")]"; 220 | } 221 | 222 | 223 | 224 | /** 225 | * Based on Nextcloud Maps's getLetterColor util. 226 | * Assigns a color to a category based on its two first letters. 227 | * 228 | * @see "https://github.com/nextcloud/maps/blob/master/src/utils.js" 229 | * @return the generated color or null for the default category 230 | */ 231 | public static int categoryColorFromName(String category) { 232 | // If category is default, return null: will be used Nextcloud's accent 233 | if (category == null || category.equals(DEFAULT_CATEGORY) || category.length() == 0) 234 | return 0; 235 | 236 | float letter1Index = category.toLowerCase().charAt(0); 237 | float letter2Index = category.toLowerCase().charAt(1); 238 | float letterCoef = ((letter1Index * letter2Index) % 100) / 100; 239 | float h = letterCoef * 360; 240 | float s = 75 + letterCoef * 10; 241 | float l = 50 + letterCoef * 10; 242 | return Color.HSVToColor( new float[]{ Math.round(h), Math.round(s), Math.round(l) }); 243 | } 244 | 245 | } 246 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/repository/GeofavoriteRepository.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.repository; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.lifecycle.MutableLiveData; 8 | 9 | import java.util.ArrayList; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | 13 | import it.danieleverducci.nextcloudmaps.api.ApiProvider; 14 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 15 | import it.danieleverducci.nextcloudmaps.utils.SingleLiveEvent; 16 | import retrofit2.Call; 17 | import retrofit2.Callback; 18 | import retrofit2.Response; 19 | 20 | /** 21 | * Singleton pattern 22 | */ 23 | public class GeofavoriteRepository { 24 | 25 | private static final String TAG = "GeofavoriteRepository"; 26 | private static GeofavoriteRepository instance; 27 | private MutableLiveData> mGeofavorites; 28 | private MutableLiveData> mCategories = new MutableLiveData>(); 29 | private MutableLiveData mIsUpdating = new MutableLiveData<>(false); 30 | private SingleLiveEvent mOnFinished = new SingleLiveEvent<>(); 31 | 32 | private Context applicationContext; 33 | 34 | public GeofavoriteRepository(Context applicationContext) { 35 | this.applicationContext = applicationContext; 36 | } 37 | 38 | public static GeofavoriteRepository getInstance(Context applicationContext) { 39 | if(instance == null){ 40 | instance = new GeofavoriteRepository(applicationContext); 41 | } 42 | return instance; 43 | } 44 | 45 | public static void resetInstance() { 46 | instance = null; 47 | } 48 | 49 | public MutableLiveData> getGeofavorites(){ 50 | if (mGeofavorites == null) { 51 | mGeofavorites = new MutableLiveData<>(); 52 | mGeofavorites.setValue(new ArrayList<>()); 53 | } 54 | return mGeofavorites; 55 | } 56 | 57 | public MutableLiveData> getCategories() { 58 | return mCategories; 59 | } 60 | 61 | public MutableLiveData isUpdating() { 62 | return mIsUpdating; 63 | } 64 | 65 | public SingleLiveEvent onFinished() { 66 | return mOnFinished; 67 | } 68 | 69 | public void updateGeofavorites() { 70 | mIsUpdating.postValue(true); 71 | // Obtain geofavorites 72 | Call> call = ApiProvider.getAPI(this.applicationContext).getGeofavorites(); 73 | call.enqueue(new Callback>() { 74 | @Override 75 | public void onResponse(@NonNull Call> call, @NonNull Response> response) { 76 | if (response.isSuccessful() && response.body() != null) { 77 | mGeofavorites.postValue(response.body()); 78 | updateCategories(response.body()); 79 | mIsUpdating.postValue(false); 80 | mOnFinished.postValue(true); 81 | } else { 82 | onFailure(call, new Throwable("Dataset is empty")); 83 | } 84 | } 85 | 86 | @Override 87 | public void onFailure(@NonNull Call> call, @NonNull Throwable t) { 88 | mIsUpdating.postValue(false); 89 | mOnFinished.postValue(false); 90 | } 91 | }); 92 | } 93 | 94 | public Geofavorite getGeofavorite(int id) { 95 | for (Geofavorite g : mGeofavorites.getValue()) { 96 | if (g.getId() == id) 97 | return g; 98 | } 99 | return null; 100 | } 101 | 102 | public void saveGeofavorite(Geofavorite geofav) { 103 | mIsUpdating.postValue(true); 104 | Call call; 105 | if (geofav.getId() == 0) { 106 | // New geofavorite 107 | call = ApiProvider.getAPI(this.applicationContext).createGeofavorite(geofav); 108 | } else { 109 | // Update existing geofavorite 110 | call = ApiProvider.getAPI(this.applicationContext).updateGeofavorite(geofav.getId(), geofav); 111 | } 112 | call.enqueue(new Callback() { 113 | @Override 114 | public void onResponse(Call call, Response response) { 115 | if (response.isSuccessful()) { 116 | List geofavs = mGeofavorites.getValue(); 117 | if (geofav.getId() != 0) { 118 | geofavs.remove(geofav); 119 | } 120 | geofavs.add(response.body()); 121 | mGeofavorites.postValue(geofavs); 122 | mIsUpdating.postValue(false); 123 | mOnFinished.postValue(true); 124 | } else { 125 | mIsUpdating.postValue(false); 126 | mOnFinished.postValue(false); 127 | } 128 | } 129 | 130 | @Override 131 | public void onFailure(Call call, Throwable t) { 132 | Log.e(TAG, t.getMessage()); 133 | mIsUpdating.postValue(false); 134 | mOnFinished.postValue(false); 135 | } 136 | }); 137 | } 138 | 139 | public void deleteGeofavorite(Geofavorite geofav) { 140 | mIsUpdating.postValue(true); 141 | // Delete Geofavorite 142 | Call call = ApiProvider.getAPI(this.applicationContext).deleteGeofavorite(geofav.getId()); 143 | call.enqueue(new Callback() { 144 | @Override 145 | public void onResponse(Call call, Response response) { 146 | List geofavs = mGeofavorites.getValue(); 147 | if (geofavs.remove(geofav)) { 148 | mGeofavorites.postValue(geofavs); 149 | mIsUpdating.postValue(false); 150 | mOnFinished.postValue(true); 151 | } else { 152 | // Should never happen 153 | mIsUpdating.postValue(false); 154 | mOnFinished.postValue(false); 155 | } 156 | } 157 | 158 | @Override 159 | public void onFailure(Call call, Throwable t) { 160 | mIsUpdating.postValue(false); 161 | mOnFinished.postValue(false); 162 | } 163 | }); 164 | } 165 | 166 | private void updateCategories(List geofavs) { 167 | HashSet categories = new HashSet<>(); 168 | for (Geofavorite g : geofavs) { 169 | String cat = g.getCategory(); 170 | if (cat != null) 171 | categories.add(cat); 172 | } 173 | mCategories.postValue(categories); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/GeoUriParser.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import android.net.Uri; 4 | import android.util.Log; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 10 | 11 | public class GeoUriParser { 12 | private static final Pattern PATTERN_GEO = Pattern.compile("geo:(-?[\\d.]+),(-?[\\d.]+)"); 13 | // Try to match not only geoUri but also Google Maps Uri 14 | private static final Pattern PATTERN_BROAD = Pattern.compile( 15 | "(?:@|&query=|&ce\nter=|geo:|#map=\\d{1,2}\\/)(-?\\d{1,2}\\.\\d+)(?:,|%2C|\\/)(-?\\d{1,3}\\.\\d{1,10})" 16 | ); 17 | 18 | /** 19 | * Parses an URI into latitude and longitude 20 | * @param uri to parse 21 | * @param strict if true, the uri must be a valid geo: uri, otherwise a broader check is applied to include other uris, like Google Maps ones 22 | * @return the parsed coordinates in an array [lat,lon] 23 | * @throws IllegalArgumentException if the url could not be parsed 24 | */ 25 | public static double[] parseUri(Uri uri, boolean strict) throws IllegalArgumentException { 26 | if (uri == null) 27 | throw new IllegalArgumentException("no uri"); 28 | 29 | // Try to extract coordinates in uri string with regexp 30 | Pattern pattern = strict ? PATTERN_GEO : PATTERN_BROAD; 31 | Matcher m = pattern.matcher(uri.toString()); 32 | if (!m.find() || m.groupCount() != 2) 33 | throw new IllegalArgumentException("unable to parse uri: unable to find coordinates in uri"); 34 | 35 | // Obtain coordinates from regexp result 36 | String sLat = m.group(1); 37 | String sLon = m.group(2); 38 | double[] coords = null; 39 | try { 40 | // Check coordinates are numeric 41 | coords = new double[]{Double.parseDouble(sLat), Double.parseDouble(sLon)}; 42 | } catch (NumberFormatException e) { 43 | throw new IllegalArgumentException("unable to parse uri: coordinates are not double"); 44 | } 45 | 46 | // Check coordinates validity 47 | String error = checkCoordsValidity(coords[0], coords[1]); 48 | if (error != null) 49 | throw new IllegalArgumentException(error); 50 | 51 | return coords; 52 | } 53 | 54 | public static Uri createGeoUri(double lat, double lon, String name) { 55 | String error = checkCoordsValidity(lat, lon); 56 | if (error != null) 57 | throw new IllegalArgumentException(error); 58 | 59 | String uriStr = "geo:" + lat + "," + lon; 60 | if (name != null) 61 | uriStr += "(" + name + ")"; 62 | return Uri.parse(uriStr); 63 | } 64 | 65 | public static Uri createGmapsUri(double lat, double lon) { 66 | String error = checkCoordsValidity(lat, lon); 67 | if (error != null) 68 | throw new IllegalArgumentException(error); 69 | 70 | String uriStr = "https://www.google.com/maps/search/?api=1&query=" + lat + "," + lon; 71 | return Uri.parse(uriStr); 72 | } 73 | 74 | /** 75 | * Checks a latitude/longitude is valid 76 | * @param lat latitude 77 | * @param lon longitude 78 | * @return null if valid, a string containing an error otherwise 79 | */ 80 | private static String checkCoordsValidity(double lat, double lon) { 81 | // Check coords validity 82 | if (lon <= -180 || lon >= 180 ) 83 | return "Invalid longitude: " + lon; 84 | if (lat <= -90 || lat >= 90) 85 | return "Invalid latitude: " + lat; 86 | return null; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/GeofavoritesFilter.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 7 | 8 | public class GeofavoritesFilter { 9 | List items; 10 | public GeofavoritesFilter(List items) { 11 | this.items = items; 12 | } 13 | 14 | public List byText(String text) { 15 | List filteredGeofavorites = new ArrayList<>(); 16 | 17 | if (text.isEmpty()) { 18 | return items; 19 | } else { 20 | for (Geofavorite geofavorite : items) { 21 | String query = text.toLowerCase(); 22 | if (geofavorite.getName() != null && geofavorite.getName().toLowerCase().contains(query)) { 23 | filteredGeofavorites.add(geofavorite); 24 | } else if (geofavorite.getComment() != null && geofavorite.getComment().toLowerCase().contains(query)) { 25 | filteredGeofavorites.add(geofavorite); 26 | } 27 | } 28 | } 29 | return filteredGeofavorites; 30 | } 31 | 32 | public List byCategory(String category) { 33 | List filteredGeofavorites = new ArrayList<>(); 34 | 35 | if (category == null) { 36 | return items; 37 | } else { 38 | for (Geofavorite geofavorite : items) { 39 | if (geofavorite.getCategory().equals(category)) { 40 | filteredGeofavorites.add(geofavorite); 41 | } 42 | } 43 | } 44 | return filteredGeofavorites; 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/IntentGenerator.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.util.Log; 7 | 8 | import it.danieleverducci.nextcloudmaps.R; 9 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 10 | 11 | public class IntentGenerator { 12 | public static Intent newShareIntent(Context context, Geofavorite item) { 13 | Intent i = new Intent(); 14 | i.setAction(Intent.ACTION_SEND); 15 | i.setType("text/plain"); 16 | String shareMessage = context.getString(R.string.share_message) 17 | .replace("{title}", item.getName() == null ? context.getString(R.string.share_message_default_title) : item.getName()) 18 | .replace("{lat}", ""+item.getLat()) 19 | .replace("{lng}", ""+item.getLng()); 20 | i.putExtra(Intent.EXTRA_TEXT, shareMessage ); 21 | return i; 22 | } 23 | 24 | public static Intent newGeoUriIntent(Context context, Geofavorite item) { 25 | Intent i = new Intent(); 26 | i.setAction(Intent.ACTION_VIEW); 27 | i.setData(isGoogleMapsInstalled(context) ? item.getGmapsUri() : item.getGeoUri()); 28 | return i; 29 | } 30 | 31 | public static boolean isGoogleMapsInstalled(Context context) { 32 | try { 33 | context.getPackageManager().getApplicationInfo("com.google.android.apps.maps", 0); 34 | return true; 35 | } catch (PackageManager.NameNotFoundException e) { 36 | return false; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/MapUtils.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import android.content.Context; 4 | import android.content.res.Configuration; 5 | 6 | import androidx.preference.PreferenceManager; 7 | 8 | import org.osmdroid.config.IConfigurationProvider; 9 | import org.osmdroid.views.MapView; 10 | import org.osmdroid.views.overlay.TilesOverlay; 11 | 12 | import it.danieleverducci.nextcloudmaps.BuildConfig; 13 | 14 | public class MapUtils { 15 | 16 | public static void configOsmdroid(Context context) { 17 | IConfigurationProvider osmdroidConfig = org.osmdroid.config.Configuration.getInstance(); 18 | osmdroidConfig.load(context, 19 | PreferenceManager.getDefaultSharedPreferences(context)); 20 | osmdroidConfig.setUserAgentValue(BuildConfig.APPLICATION_ID); 21 | } 22 | 23 | public static void setTheme(MapView mapView) { 24 | int currentNightMode = mapView.getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; 25 | switch (currentNightMode) { 26 | case Configuration.UI_MODE_NIGHT_NO: 27 | // Night mode is not active, we're using the light theme 28 | mapView.getOverlayManager().getTilesOverlay().setColorFilter(null); 29 | break; 30 | case Configuration.UI_MODE_NIGHT_YES: 31 | // Night mode is active, we're using dark theme 32 | mapView.getOverlayManager().getTilesOverlay().setColorFilter(TilesOverlay.INVERT_COLORS); 33 | break; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/SettingsManager.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import androidx.preference.PreferenceManager; 7 | import it.danieleverducci.nextcloudmaps.activity.main.GeofavoriteAdapter; 8 | 9 | public class SettingsManager { 10 | static private final String SETTING_SORT_BY = "SETTING_SORT_BY"; 11 | static private final String SETTING_LAST_SELECTED_LIST_VIEW = "SETTING_LAST_SELECTED_LIST_VIEW"; 12 | static private final String SETTING_LAST_MAP_POSITION_LAT = "SETTING_LAST_MAP_POSITION_LAT"; 13 | static private final String SETTING_LAST_MAP_POSITION_LNG = "SETTING_LAST_MAP_POSITION_LNG"; 14 | static private final String SETTING_LAST_MAP_POSITION_ZOOM = "SETTING_LAST_MAP_POSITION_ZOOM"; 15 | 16 | public static int getGeofavoriteListSortBy(Context context) { 17 | return PreferenceManager.getDefaultSharedPreferences(context) 18 | .getInt(SETTING_SORT_BY, GeofavoriteAdapter.SORT_BY_CREATED); 19 | } 20 | 21 | public static void setGeofavoriteListSortBy(Context context, int value) { 22 | PreferenceManager.getDefaultSharedPreferences(context) 23 | .edit().putInt(SETTING_SORT_BY, value).apply(); 24 | } 25 | 26 | public static boolean isGeofavoriteListShownAsMap(Context context) { 27 | return PreferenceManager.getDefaultSharedPreferences(context) 28 | .getBoolean(SETTING_LAST_SELECTED_LIST_VIEW, false); 29 | } 30 | 31 | public static void setGeofavoriteListShownAsMap(Context context, boolean value) { 32 | PreferenceManager.getDefaultSharedPreferences(context) 33 | .edit().putBoolean(SETTING_LAST_SELECTED_LIST_VIEW, value).apply(); 34 | } 35 | 36 | /** 37 | * Returns the last saved position 38 | * @param context 39 | * @return double[lat,lng,zoom] 40 | */ 41 | public static double[] getLastMapPosition(Context context) { 42 | SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 43 | return new double[] { 44 | (double) sp.getFloat(SETTING_LAST_MAP_POSITION_LAT, 0.0f), 45 | (double) sp.getFloat(SETTING_LAST_MAP_POSITION_LNG, 0.0f), 46 | (double) sp.getFloat(SETTING_LAST_MAP_POSITION_ZOOM, 10.0f), 47 | }; 48 | } 49 | 50 | public static void setLastMapPosition(Context context, double lat, double lng, double zoom) { 51 | PreferenceManager.getDefaultSharedPreferences(context).edit() 52 | .putFloat(SETTING_LAST_MAP_POSITION_LAT, (float)lat) 53 | .putFloat(SETTING_LAST_MAP_POSITION_LNG, (float)lng) 54 | .putFloat(SETTING_LAST_MAP_POSITION_ZOOM, (float)zoom) 55 | .apply(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/utils/SingleLiveEvent.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.utils; 2 | 3 | import androidx.annotation.MainThread; 4 | import androidx.annotation.NonNull; 5 | import androidx.lifecycle.LifecycleOwner; 6 | import androidx.lifecycle.MutableLiveData; 7 | import androidx.lifecycle.Observer; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | 10 | /** 11 | * Events implementation in LiveData 12 | * From: https://gist.github.com/teegarcs/319a3e7e4736a0cce8eba2216c52b0ca#file-singleliveevent 13 | * @param 14 | */ 15 | public class SingleLiveEvent extends MutableLiveData { 16 | AtomicBoolean mPending = new AtomicBoolean(false); 17 | 18 | @MainThread 19 | @Override 20 | public void setValue(T value) { 21 | mPending.set(true); 22 | super.setValue(value); 23 | } 24 | 25 | @MainThread 26 | @Override 27 | public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { 28 | super.observe(owner, new Observer() { 29 | @Override 30 | public void onChanged(T t) { 31 | if (mPending.compareAndSet(true, false)) { 32 | observer.onChanged(t); 33 | } 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Util function for Void implementations. 40 | */ 41 | public void call() { 42 | setValue(null); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/it/danieleverducci/nextcloudmaps/views/GeofavMarkerInfoWindow.java: -------------------------------------------------------------------------------- 1 | package it.danieleverducci.nextcloudmaps.views; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.view.View; 6 | 7 | import androidx.appcompat.content.res.AppCompatResources; 8 | import androidx.core.graphics.drawable.DrawableCompat; 9 | 10 | import org.osmdroid.views.MapView; 11 | import org.osmdroid.views.overlay.infowindow.InfoWindow; 12 | import org.osmdroid.views.overlay.infowindow.MarkerInfoWindow; 13 | 14 | import it.danieleverducci.nextcloudmaps.R; 15 | import it.danieleverducci.nextcloudmaps.model.Geofavorite; 16 | 17 | public class GeofavMarkerInfoWindow extends MarkerInfoWindow implements View.OnClickListener { 18 | private OnGeofavMarkerInfoWindowClickListener onGeofavMarkerInfoWindowClickListener; 19 | 20 | public GeofavMarkerInfoWindow(MapView mapView, Geofavorite geofavorite) { 21 | super(R.layout.infowindow_geofav, mapView); 22 | Context context = getView().getContext(); 23 | 24 | // Set category color 25 | View category = getView().findViewById(R.id.bubble_subdescription); 26 | Drawable backgroundDrawable = category.getBackground(); 27 | DrawableCompat.setTint(backgroundDrawable, geofavorite.categoryColor() == 0 ? context.getColor(R.color.defaultBrand) : geofavorite.categoryColor()); 28 | 29 | // Set listeners 30 | getView().findViewById(R.id.action_icon_share).setOnClickListener(this); 31 | getView().findViewById(R.id.action_icon_nav).setOnClickListener(this); 32 | getView().findViewById(R.id.action_icon_delete).setOnClickListener(this); 33 | getView().findViewById(R.id.action_icon_edit).setOnClickListener(this); 34 | } 35 | 36 | public void setOnGeofavMarkerInfoWindowClickListener(OnGeofavMarkerInfoWindowClickListener l) { 37 | this.onGeofavMarkerInfoWindowClickListener = l; 38 | } 39 | 40 | @Override 41 | public void onOpen(Object item) { 42 | InfoWindow.closeAllInfoWindowsOn(getMapView()); 43 | super.onOpen(item); 44 | } 45 | 46 | 47 | @Override 48 | public void onClick(View v) { 49 | if (onGeofavMarkerInfoWindowClickListener == null) 50 | return; 51 | 52 | if (v.getId() == R.id.action_icon_share) 53 | onGeofavMarkerInfoWindowClickListener.onGeofavMarkerInfoWindowShareClick(); 54 | 55 | if (v.getId() == R.id.action_icon_nav) 56 | onGeofavMarkerInfoWindowClickListener.onGeofavMarkerInfoWindowNavClick(); 57 | 58 | if (v.getId() == R.id.action_icon_delete) 59 | onGeofavMarkerInfoWindowClickListener.onGeofavMarkerInfoWindowDeleteClick(); 60 | 61 | if (v.getId() == R.id.action_icon_edit) 62 | onGeofavMarkerInfoWindowClickListener.onGeofavMarkerInfoWindowEditClick(); 63 | } 64 | 65 | 66 | public interface OnGeofavMarkerInfoWindowClickListener { 67 | public void onGeofavMarkerInfoWindowEditClick(); 68 | public void onGeofavMarkerInfoWindowShareClick(); 69 | public void onGeofavMarkerInfoWindowNavClick(); 70 | public void onGeofavMarkerInfoWindowDeleteClick(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/floating_semitransparent_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/geofav_infowindow_pointer.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_accuracy_fail.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_accuracy_ok.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_gps.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_map.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_alphabetical_asc.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_app.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_category_asc.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_distance_asc.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter_off.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_list_pin.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_logout_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_map_pin.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_modification_asc.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_ok.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_time_grey.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_view_list.xml: -------------------------------------------------------------------------------- 1 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_view_map.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/infowindow_geofav_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_label_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/unselected_floating_semitransparent_button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/user_badge_mask.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 26 | 27 | 31 | 32 | 39 | 40 | 45 | 46 | 55 | 56 | 61 | 62 | 69 | 70 | 75 | 76 | 82 | 83 |