Mobile TP8

This commit is contained in:
2026-01-25 16:11:02 +01:00
commit 726acd3e09
60 changed files with 1846 additions and 0 deletions

1
app/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

48
app/build.gradle.kts Normal file
View File

@@ -0,0 +1,48 @@
plugins {
alias(libs.plugins.android.application)
}
android {
namespace = "com.example.tp8"
compileSdk {
version = release(36)
}
defaultConfig {
applicationId = "com.example.tp8"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.activity)
implementation(libs.constraintlayout)
implementation("androidx.exifinterface:exifinterface:1.3.7")
implementation("com.google.android.gms:play-services-location:21.2.0")
implementation(libs.exifinterface)
implementation(libs.play.services.location)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

21
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,26 @@
package com.example.tp8;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.example.tp8", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TP8">
<activity
android:name=".NearbyPhotosActivity"
android:exported="false" />
<activity
android:name=".PhotoListActivity"
android:exported="false" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,148 @@
package com.example.tp8;
import android.content.Intent;
import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.exifinterface.media.ExifInterface;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Locale;
public class MainActivity extends AppCompatActivity {
private ImageView imageView;
private TextView latiTextView;
private TextView longiTextView;
private TextView locationTextView;
private Button loadImageButton;
private Button showLocationButton;
private Button findNearbyPhotosButton; // Exo3
private double[] currentLatLong;
// Result launcher pour gérer la récupération et l'affichage de l'image choisie
private final ActivityResultLauncher<String> getImageLauncher = registerForActivityResult(
new ActivityResultContracts.GetContent(), // Récupérer le chemin de l'image
uri -> {
if (uri != null) { // S'il y a bien une image
imageView.setImageURI(uri); // L'afficher sur son emplacement
showExifData(uri); // Puis récupérer les coords GPS si possible
}
}
);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
imageView = findViewById(R.id.imageView); // Récupération des éléments de l'interface
latiTextView = findViewById(R.id.lati);
longiTextView = findViewById(R.id.longi);
locationTextView = findViewById(R.id.location);
loadImageButton = findViewById(R.id.button_load_image);
showLocationButton = findViewById(R.id.button_show_location); // Bouton caché au lancement, affiché si des coords GPS sont trouvées
findNearbyPhotosButton = findViewById(R.id.button_find_nearby_photos); // Bouton exo3
Button findPhotosByUserLocationButton = findViewById(R.id.button_find_photos_by_user_location); // Bouton exo4
loadImageButton.setOnClickListener(event -> { // Quand le bouton de chargement est utilisé
getImageLauncher.launch("image/*"); // On lance la demande d'image
});
showLocationButton.setOnClickListener(event -> {
if (currentLatLong != null) {
reverseGeocode(currentLatLong[0], currentLatLong[1]); // GPS -> Lieu
}
});
findNearbyPhotosButton.setOnClickListener(event -> {
if (currentLatLong != null) {
Intent intent = new Intent(MainActivity.this, PhotoListActivity.class);
intent.putExtra("referenceLatLong", currentLatLong); // Passer les coordonnées de la photo actuelle
startActivity(intent);
}
});
findPhotosByUserLocationButton.setOnClickListener(event -> {
Intent intent = new Intent(MainActivity.this, NearbyPhotosActivity.class);
startActivity(intent);
});
}
private void showExifData(Uri imageUri) { // Fonction qui extrait les coords GPS
try (InputStream inputStream = getContentResolver().openInputStream(imageUri)) {
// J'essaie de récupérer les données de l'image
if (inputStream != null) {
ExifInterface exifInterface = new ExifInterface(inputStream); // Permet d'intéragir avec l'image
currentLatLong = exifInterface.getLatLong(); // Obtenir les coordonnées
if (currentLatLong != null) { // Si les coords sont bien définies https://developer.android.com/reference/android/media/ExifInterface#getLatLong(float[])
latiTextView.setText(getString(R.string.lati) + " " + currentLatLong[0]); // Je peux les écrire
longiTextView.setText(getString(R.string.longi) + " " + currentLatLong[1]);
showLocationButton.setVisibility(View.VISIBLE); // Afficher le bouton pour trouver le lieu
findNearbyPhotosButton.setVisibility(View.VISIBLE); // Afficher le bouton pour trouver les photos proches
} else {
latiTextView.setText(getString(R.string.lati) + " Inconnue"); // Sinon, inconnu
longiTextView.setText(getString(R.string.longi) + " Inconnue");
showLocationButton.setVisibility(View.GONE); // Cacher le bouton pour trouver le lieu
findNearbyPhotosButton.setVisibility(View.GONE); // Cacher le bouton pour trouver les photos proches
}
locationTextView.setText(""); // Retirer le lieu affiché
}
} catch (IOException e) { // En cas de problème avec la récupération de l'image ou ses données
e.printStackTrace(); // J'affiche l'erreur
latiTextView.setText(getString(R.string.lati) + " Erreur");
longiTextView.setText(getString(R.string.longi) + " Erreur");
showLocationButton.setVisibility(View.GONE);
findNearbyPhotosButton.setVisibility(View.GONE);
}
}
private void reverseGeocode(double latitude, double longitude) {
// DOC: https://developer.android.com/reference/android/location/Geocoder
new Thread(() -> { // Pour ne pas bloquer l'application, je lance la récupération dans un sous-processus
Geocoder geocoder = new Geocoder(this, Locale.getDefault());
try {
List<Address> addresses = geocoder.getFromLocation(latitude, longitude, 1); // J'essaie de récupérer 1 adresse qui correspond
if (addresses != null && !addresses.isEmpty()) { // Si l'on a trouvé quelque chose
Address address = addresses.get(0);
StringBuilder addressText = new StringBuilder(); // Je prépare une String
if (address.getLocality() != null) { // Si j'ai la bille
addressText.append(address.getLocality()).append(", "); // J'ajoute à la String
}
if (address.getCountryName() != null) {
addressText.append(address.getCountryName()); // Idem pour le pays
}
// https://stackoverflow.com/a/11140342 pour retourner les données vers le thread principal
runOnUiThread(() -> locationTextView.setText(addressText.toString())); // Et je retourne ça au thread principal
} else {
runOnUiThread(() -> locationTextView.setText(getString(R.string.unknown_place))); // Pas d'adresse trouvée
}
} catch (IOException e) {
e.printStackTrace(); // Crash
runOnUiThread(() -> locationTextView.setText(getString(R.string.error_place)));
}
}).start();
}
}

View File

@@ -0,0 +1,256 @@
package com.example.tp8;
import android.Manifest;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.exifinterface.media.ExifInterface;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.DiffUtil;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationServices;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class NearbyPhotosActivity extends AppCompatActivity {
private static final int PERMISSIONS_REQUEST_LOCATION_AND_MEDIA = 101; // ID de la requête de permission
private FusedLocationProviderClient fusedLocationClient; //https://developers.google.com/android/reference/com/google/android/gms/location/FusedLocationProviderClient
private Location currentUserLocation;
private EditText distanceInput;
private Button searchButton;
private RecyclerView recyclerView;
private NearbyPhotoAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nearby_photos);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.photosAroundBackground), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
distanceInput = findViewById(R.id.distance_input); // éléménts de l'interface
searchButton = findViewById(R.id.search_button);
recyclerView = findViewById(R.id.nearby_photos_recycler_view);
// Fournisseur de localisation
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); // Permet de gérer les deux types de position sans devoir y prêter attention
// RecyclerView qui gère l'affichage des éléments, mits en page selon GridLayoutManager
recyclerView.setLayoutManager(new GridLayoutManager(this, 3)); // 3 images par ligne dans la grille
adapter = new NearbyPhotoAdapter(); // Adapteur local pour les photos
recyclerView.setAdapter(adapter);
searchButton.setOnClickListener(v -> { // Click bouton recherche
String distanceStr = distanceInput.getText().toString(); // Je récupère la distance de recherche
if (currentUserLocation == null) {
Toast.makeText(this, getText(R.string.no_position_available), Toast.LENGTH_SHORT).show(); // Impossible d'avoir la position de l'utilisateur
return;
}
if (distanceStr.isEmpty()) { // Pas de distance entrée pour le moment
Toast.makeText(this, getText(R.string.input_distance), Toast.LENGTH_SHORT).show();
return;
}
try { // Et j'essaie de convertir la distance en nombre0
double maxDistance = Double.parseDouble(distanceStr); // Distance maximale de recherche
loadNearbyPhotos(maxDistance); // Et je recherche les images dans ce rayon
} catch (NumberFormatException e) {
Toast.makeText(this, getText(R.string.invalid_distance), Toast.LENGTH_SHORT).show(); // Nombre invalide
}
}); // Fin listener bouton
checkPermissionsAndGetLocation(); // Au lancement, je vérifie les permissions et obtiens la position
}
// Permission et position
private void checkPermissionsAndGetLocation() {
List<String> permissionsToRequest = new ArrayList<>(); // Liste des permissions qui seront demandées
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.ACCESS_FINE_LOCATION); // Si je n'ai pas l'accès à la position précise, je demande
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Suggéré par l'IDE pour gérer tous les cas de versions Android
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.READ_MEDIA_IMAGES);
}
} else {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_MEDIA_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// Voir la position des images dans les métadonnées
permissionsToRequest.add(Manifest.permission.ACCESS_MEDIA_LOCATION);
}
if (!permissionsToRequest.isEmpty()) {
// S'il y a des permissions à demander, j'envois la demande
ActivityCompat.requestPermissions(this, permissionsToRequest.toArray(new String[0]), PERMISSIONS_REQUEST_LOCATION_AND_MEDIA);
} else {
fetchUserLocation(); // J'ai tout, je récupère la position de l'utilisateur
}
}
@Override // Résultat de la demande de permission
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSIONS_REQUEST_LOCATION_AND_MEDIA) {
boolean allGranted = true;
for (int grantResult : grantResults) { // Vérifier que tous les accès ont été donnés
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
fetchUserLocation(); // Tout y est, je récupère la position
} else {
Toast.makeText(this, getString(R.string.missing_permission), Toast.LENGTH_LONG).show(); // Il manque des accès
finish();
}
}
}
private void fetchUserLocation() { // Récupération de la positon de l'utilisateur
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
return; // Echec si je n'ai aucun accès à la position
}
fusedLocationClient.getLastLocation() // Récupérer la position
.addOnSuccessListener(this, location -> {
if (location != null) {
currentUserLocation = location; // Position récupérée
Toast.makeText(this, getString(R.string.position_got), Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, getString(R.string.position_error), Toast.LENGTH_LONG).show(); // Erreur
}
});
}
private void loadNearbyPhotos(double maxDistance) { // Chargement des photos proches du lieu
new Thread(() -> { // Dans un nouveau thread
List<Uri> nearbyPhotos = new ArrayList<>();
ContentResolver contentResolver = getContentResolver(); // Je prépare de quoi chercher dans le stockage
Uri queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
String[] projection = {MediaStore.Images.Media._ID};
try (Cursor cursor = contentResolver.query(queryUri, projection, null, null, null)) {
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); // Récupérer l'ID de la colone qui gère les images
while (cursor.moveToNext()) { // Tant qu'il reste des images à récupérer
long id = cursor.getLong(idColumn);
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Long.toString(id)); // Puis je récupère l'URI de l'image
try (InputStream inputStream = contentResolver.openInputStream(photoUri)) { // Je tente de récupérer l'ExifInterfacve
if (inputStream != null) {
ExifInterface exifInterface = new ExifInterface(inputStream);
double[] latLong = exifInterface.getLatLong(); // Récupérer les coords
if (latLong != null) { // Si elles existent
double distance = distance(currentUserLocation.getLatitude(), currentUserLocation.getLongitude(), latLong[0], latLong[1]);// Calcul distance km
//System.out.println(currentUserLocation.getLatitude()+"/"+currentUserLocation.getLongitude());
//System.out.println(maxDistance);
if (distance <= maxDistance) {
// OK, dans le rayon de recherche
nearbyPhotos.add(photoUri);
}
}
}
} catch (Exception e) {
// Impossible de récupérer ExifInterface, je passe cette image
}
}
}
}
runOnUiThread(() -> { // Une fois que j'ai finit de récupérer les images
// Dans le thread principal
adapter.submitList(nearbyPhotos); // J'envois la liste à l'adapter
if(nearbyPhotos.isEmpty()){
Toast.makeText(this, getString(R.string.no_pictures), Toast.LENGTH_SHORT).show();
}
});
}).start();
}
// J'aurai aussi pu passer par https://developer.android.com/reference/android/location/Location#distanceBetween(double,%20double,%20double,%20double,%20float[])
private static double distance(double lat1, double lon1, double lat2, double lon2) {
double R = 6371; // Rayon de la Terre, utilisé pour https://fr.wikipedia.org/wiki/Formule_de_haversine
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private static class NearbyPhotoAdapter extends ListAdapter<Uri, NearbyPhotoAdapter.ViewHolder> {
//Adapteur pour l'affichage en grille des images
protected NearbyPhotoAdapter() {
super(DIFF_CALLBACK); // Implémentation du diff callback de https://developer.android.com/reference/androidx/leanback/widget/DiffCallback
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// Créer l'afifchage en utilisant le layour en grille
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.grid_item_photo, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.imageView.setImageURI(getItem(position)); // Ajouter une image à une position dans la grille
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public ViewHolder(View view) {
super(view);
imageView = view.findViewById(R.id.photo_grid_item);
}
}
}
private static final DiffUtil.ItemCallback<Uri> DIFF_CALLBACK = // Android se sert de cette partie pour comparer les éléments https://developer.android.com/reference/androidx/leanback/widget/DiffCallback
new DiffUtil.ItemCallback<Uri>() {
@Override
public boolean areItemsTheSame(@NonNull Uri oldItem, @NonNull Uri newItem) {
return oldItem.equals(newItem);
}
@Override
public boolean areContentsTheSame(@NonNull Uri oldItem, @NonNull Uri newItem) {
return oldItem.equals(newItem);
}
};
}

View File

@@ -0,0 +1,249 @@
package com.example.tp8;
import android.Manifest;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.exifinterface.media.ExifInterface;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
public class PhotoListActivity extends AppCompatActivity {
private static final int PERMISSIONS_REQUEST_READ_IMAGES = 100; // ID de la requête de permission
private PhotoAdapter adapter;
private double[] referenceLatLong;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_photo_list);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.photo_list_layout), (v, insets) -> { // Changed R.id.main to R.id.photo_list_layout
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
referenceLatLong = getIntent().getDoubleArrayExtra("referenceLatLong"); // Je récupère la valeur de référence pour la position GPS
if (referenceLatLong == null) {
finish(); // Ne devrait pas se produire normalement
return;
}
RecyclerView recyclerView = findViewById(R.id.photo_recycler_view); // https://developer.android.com/develop/ui/views/layout/recyclerview?hl=fr
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new PhotoAdapter(); // Affichage d'une liste dynamique de photos
recyclerView.setAdapter(adapter);
checkPermissionAndLoadPhotos(); // Vérification des permissions, puis chargement des photos
}
private void checkPermissionAndLoadPhotos() {
List<String> permissionsToRequest = new ArrayList<>();
// Choisir la permission de stockage appropriée
String storagePermission;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // If-else recommandé par Android Studio pour gérer toutes les versions d'Android
storagePermission = Manifest.permission.READ_MEDIA_IMAGES; // https://developer.android.com/about/versions/14/changes/partial-photo-video-access?hl=fr
} else {
storagePermission = Manifest.permission.READ_EXTERNAL_STORAGE;
}
if (ContextCompat.checkSelfPermission(this, storagePermission) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(storagePermission); // Je n'ai pas la permission, j'ajoute à la liste de perms à demander
}
// Ajouter la permission pour la localisation des médias, pour lire les métadonnées des images
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_MEDIA_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(Manifest.permission.ACCESS_MEDIA_LOCATION); // J'ajoute aussi si je ne l'ai pas
}
if (!permissionsToRequest.isEmpty()) {
// Demander les permissions nécessaires
ActivityCompat.requestPermissions(this, permissionsToRequest.toArray(new String[0]), PERMISSIONS_REQUEST_READ_IMAGES);
} else {
// OK, toutes les permissions sont déjà accordées
loadPhotos();
}
}
@Override // Fonction appelée quand le système a reçu une réponse de la demande de permission
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSIONS_REQUEST_READ_IMAGES) {
// Vérifier si toutes les permissions demandées ont été accordées
boolean allGranted = true;
for (int grantResult : grantResults) {
if (grantResult != PackageManager.PERMISSION_GRANTED) {
allGranted = false;
break;
}
}
if (allGranted) {
// Permissions OK
loadPhotos();
} else {
// Au moins une permission a été refusée
Toast.makeText(this, getText(R.string.missing_permission), Toast.LENGTH_LONG).show();
finish(); // Terminer ici
}
}
}
// Fonction de chargement des photos
private void loadPhotos() {
new Thread(() -> { // Dans un thread, comme vu dans la doc' Android
List<PhotoItem> photoItems = new ArrayList<>(); // Liste principale des images
ContentResolver contentResolver = getContentResolver();
Uri queryUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; // Champ recherché
String[] projection = {MediaStore.Images.Media._ID};
try (Cursor cursor = contentResolver.query(queryUri, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC")) { // Je tente de prioriser les images récentes
if (cursor != null) {
int idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID); // ID colone rechercheé
while (cursor.moveToNext()) { // Tant qu'il y a des images
long id = cursor.getLong(idColumn);
Uri photoUri = Uri.withAppendedPath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, Long.toString(id)); // Récupérer l'URI
try (InputStream inputStream = contentResolver.openInputStream(photoUri)) { // Je tente d'avoir l'InputStream
if (inputStream != null) {
ExifInterface exifInterface = new ExifInterface(inputStream); // Que je convertis en ExifInterface
double[] latLong = exifInterface.getLatLong();
if (latLong != null) {
double distance = distance(referenceLatLong[0], referenceLatLong[1], latLong[0], latLong[1]); // D'où je récupère la distance
photoItems.add(new PhotoItem(photoUri, distance)); // Et j'ajoute ça à la liste
}
}
} catch (IOException e) {
// J'ignore les images impossibles à lire, possiblement corrompues
} catch (Exception e) {
// Juste au cas où, je collecte ici tout le reste pour le débug
e.printStackTrace();
}
}
}
}
Collections.sort(photoItems); // Je lance un tri sur la liste de photos (méthode de tri sur la distance)
runOnUiThread(() -> {
if (photoItems.isEmpty()) { // Message s'il n'y a pas de photos
Toast.makeText(PhotoListActivity.this, getString(R.string.no_pictures_found), Toast.LENGTH_LONG).show();
}
adapter.submitList(photoItems);// Puis envoi de la liste
});
}).start();
}
// J'aurai aussi pu passer par https://developer.android.com/reference/android/location/Location#distanceBetween(double,%20double,%20double,%20double,%20float[])
private static double distance(double lat1, double lon1, double lat2, double lon2) {
double R = 6371; // Rayon de la Terre, utilisé pour https://fr.wikipedia.org/wiki/Formule_de_haversine
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private static class PhotoItem implements Comparable<PhotoItem> {
final Uri uri;
final double distance;
PhotoItem(Uri uri, double distance) {
this.uri = uri;
this.distance = distance;
}
@Override
public int compareTo(PhotoItem other) {
return Double.compare(this.distance, other.distance);
}
}
// Classe que je garde là pour gérer proprement les photos
private static class PhotoAdapter extends ListAdapter<PhotoItem, PhotoAdapter.PhotoViewHolder> {
// https://developer.android.com/reference/androidx/recyclerview/widget/ListAdapter
protected PhotoAdapter() {
super(DIFF_CALLBACK); // utilisé par Android pour savoir s'il faut rebuild la liste
}
@NonNull
@Override // Afficher les images à la création du widget
public PhotoAdapter.PhotoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_photo, parent, false);
return new PhotoAdapter.PhotoViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull PhotoAdapter.PhotoViewHolder holder, int position) {
PhotoItem item = getItem(position);
holder.bind(item);
}
static class PhotoViewHolder extends RecyclerView.ViewHolder {
// La sous-classe qui gère quelques méthodes d'affichage des images
final ImageView thumbnail;
final TextView distance;
PhotoViewHolder(@NonNull View itemView) {
super(itemView);
thumbnail = itemView.findViewById(R.id.photo_thumbnail);
distance = itemView.findViewById(R.id.photo_distance);
}
void bind(PhotoItem item) {
thumbnail.setImageURI(item.uri); // Image affichée
distance.setText(String.format(Locale.getDefault(), "Distance: %.2f km", item.distance)); // Texte
}
}
private static final DiffUtil.ItemCallback<PhotoItem> DIFF_CALLBACK = // Android se sert de cette partie pour comparer les éléments https://developer.android.com/reference/androidx/leanback/widget/DiffCallback
new DiffUtil.ItemCallback<PhotoItem>() {
@Override
public boolean areItemsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) {
return oldItem.uri.equals(newItem.uri);
}
@Override
public boolean areContentsTheSame(@NonNull PhotoItem oldItem, @NonNull PhotoItem newItem) {
return oldItem.distance == newItem.distance;
}
};
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/app_name"
app:titleTextColor="#E9DEDE" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="16dp"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<Button
android:id="@+id/button_load_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Charger une image"/>
<TextView
android:id="@+id/lati"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/lati" />
<TextView
android:id="@+id/longi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/longi" />
<Button
android:id="@+id/button_show_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/show_place"
android:visibility="gone" />
<TextView
android:id="@+id/location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"/>
<Button
android:id="@+id/button_find_nearby_photos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/find_nearby_photos"
android:visibility="gone"/>
<Button
android:id="@+id/button_find_photos_by_user_location"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/find_photos_by_user_location"
android:visibility="visible"/>
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
android:contentDescription="Image sélectionnée"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/photosAroundBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".NearbyPhotosActivity">
<EditText
android:id="@+id/distance_input"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:hint="@string/km_distance"
android:inputType="numberDecimal"
app:layout_constraintEnd_toStartOf="@+id/search_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nearby_photos_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/distance_input" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/photo_list_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".PhotoListActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/photo_recycler_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/photo_grid_item"
android:layout_width="match_parent"
android:layout_height="120dp"
android:scaleType="centerCrop"
android:contentDescription="Photo" />

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageView
android:id="@+id/photo_thumbnail"
android:layout_width="100dp"
android:layout_height="100dp"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/photo_distance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:textSize="16sp" />
</LinearLayout>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.TP8" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,23 @@
<resources>
<string name="app_name">TP8 Taccoen</string>
<string name="lati">Latitude:</string>
<string name="longi">Longitude:</string>
<string name="show_place">Afficher le lieu</string>
<string name="unknown_place">Lieu inconnu</string>
<string name="error_place">Erreur de lieu</string>
<string name="missing_permission">Permission refusée</string>
<string name="no_pictures_found">Aucune image trouvée</string>
<string name="find_nearby_photos">Trouver des images proches</string>
<string name="km_distance">Distance en km</string>
<string name="search">Rechercher</string>
<string name="find_photos_by_user_location">Photos proches de moi</string>
<string name="no_position_available">Localisation indisponible</string>
<string name="input_distance">Entrez une distance</string>
<string name="invalid_distance">Distance invalide</string>
<string name="position_got">Position obtenue</string>
<string name="position_error">Erreur d\'obtention de la position</string>
<string name="no_pictures">Aucune image trouvée</string>
</resources>

View File

@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.TP8" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.TP8" parent="Base.Theme.TP8" />
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@@ -0,0 +1,17 @@
package com.example.tp8;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}