diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2cbea79..38f082e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + suspend fun getResponse(): Response + + @GET("gempaterkini.json") + suspend fun getResponseBigMagnitude(): Response companion object { - var apiInterface : API? = null + var apiInterface: API? = null var BASE_URL = "https://data.bmkg.go.id/DataMKG/TEWS/" - fun create() : API { + fun create(): API { if (apiInterface == null) { val retrofit = Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) diff --git a/app/src/main/java/io/chronostech/awasgempabumi/EarthQuakeRepo.kt b/app/src/main/java/io/chronostech/awasgempabumi/EarthQuakeRepo.kt index dc5466a..d759ec2 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/EarthQuakeRepo.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/EarthQuakeRepo.kt @@ -1,16 +1,13 @@ package io.chronostech.awasgempabumi -import android.app.Application -import androidx.lifecycle.MutableLiveData import io.chronostech.awasgempabumi.model.EarthQuakeResponse -import io.chronostech.awasgempabumi.model.Gempa -import io.chronostech.awasgempabumi.model.Infogempa -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import javax.inject.Inject class EarthQuakeRepo @Inject constructor(private val retrofitService : API) { suspend fun getResponse(): Response = retrofitService.getResponse() + + suspend fun getResponseBigMagnitude(): Response = + retrofitService.getResponseBigMagnitude() } diff --git a/app/src/main/java/io/chronostech/awasgempabumi/Util.kt b/app/src/main/java/io/chronostech/awasgempabumi/Util.kt index 9517dc4..60aac37 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/Util.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/Util.kt @@ -7,14 +7,61 @@ class Util { companion object { private val acehBound = LatLngBounds(LatLng(2.00, 95.00), LatLng(6.00, 99.00)) private val sumutBound = LatLngBounds(LatLng(1.00, 97.00), LatLng(5.00, 101.00)) + private val sumbarBound = LatLngBounds(LatLng(1.00, 98.00), LatLng(4.00, 102.00)) + private val riauBound = LatLngBounds(LatLng(2.00, 100.00), LatLng(3.00, 109.00)) + private val kepriBound = LatLngBounds(LatLng(-3.00, 101.00), LatLng(-1.00, 104.00)) + private val jambiBound = LatLngBounds(LatLng(-3.00, 101.00), LatLng(-1.00, 105.00)) + private val sumselBound = LatLngBounds(LatLng(-5.00, 102.00), LatLng(-1.00, 107.00)) + private val bengkuluBound = LatLngBounds(LatLng(-6.00, 101.00), LatLng(-2.00, 104.00)) + private val lampungBound = LatLngBounds(LatLng(-7.00, 103.00), LatLng(-3.00, 106.00)) + private val babelBound = LatLngBounds(LatLng(-4.00, 105.00), LatLng(-1.00, 109.00)) + private val jakartaBound = LatLngBounds(LatLng(-7.00, 106.00), LatLng(-6.00, 107.00)) + private val jabarBound = LatLngBounds(LatLng(-8.00, 106.00), LatLng(-5.00, 109.00)) + private val bantenBound = LatLngBounds(LatLng(-8.00, 105.00), LatLng(-5.00, 107.00)) + private val jatengBound = LatLngBounds(LatLng(-9.00, 108.00), LatLng(-6.00, 112.00)) + private val jogjaBound = LatLngBounds(LatLng(-9.00, 110.00), LatLng(-7.00, 111.00)) + private val jatimBound = LatLngBounds(LatLng(-9.00, 110.00), LatLng(-6.00, 115.00)) + private val baliBound = LatLngBounds(LatLng(-9.00, 114.00), LatLng(-8.00, 116.00)) + private val ntbBound = LatLngBounds(LatLng(-10.00, 115.00), LatLng(-8.00, 120.00)) + private val nttBound = LatLngBounds(LatLng(-11.00, 118.00), LatLng(-8.00, 126.00)) + + // 27 . Sulawesi Tengah : 2ºLU-4ºLS dan 119º-125ºBT +// +// 28 . Sulawesi Selatan : 0ºLS-8ºLS dan 118º-122ºBT +// +// 29. Sulawesi Barat: 0ºLS-3ºLS dan 118º-120ºBT +// +// 30 . Sulawesi Tenggara : 2ºLS-7ºLS dan 120º-125ºBT +// +// 31 . Maluku : 0ºLS-9ºLS dan 124º-136ºBT +// +// 32 . Maluku utara : 3ºLU-º3LS dan 124º-129ºBT + private val sulutBound = LatLngBounds(LatLng(0.00, 118.00), LatLng(8.00, 112.00)) + private val gorontaloBound = LatLngBounds(LatLng(0.00, 120.00), LatLng(1.00, 124.00)) private val papuaBound = LatLngBounds(LatLng(-6.00, 131.00), LatLng(-1.00, 141.00)) private val papuaBaratBound = LatLngBounds(LatLng(-5.00, 130.00), LatLng(0.00, 138.00)) val province = mapOf( "Aceh" to acehBound, "Sumatra Utara" to sumutBound, + "Sumatra Barat" to sumbarBound, + "Riau" to riauBound, + "Kepulauan Riau" to kepriBound, + "Jambi" to jambiBound, + "Sumatra Selatan" to sumselBound, + "Bengkulu" to bengkuluBound, + "Lampung" to lampungBound, + "Bangka Belitung" to babelBound, "Jakarta" to jakartaBound, + "Jawa Barat" to jabarBound, + "Banten" to bantenBound, + "Jawa Tengah" to jatengBound, + "Jawa Timur" to jatimBound, + "DIY Jogyakarta" to jogjaBound, + "Bali" to baliBound, + "NTB" to ntbBound, + "NTT" to nttBound, "Papua" to papuaBound, "Papua Barat" to papuaBaratBound ) diff --git a/app/src/main/java/io/chronostech/awasgempabumi/model/Gempa.kt b/app/src/main/java/io/chronostech/awasgempabumi/model/Gempa.kt index 6f53e9d..4881571 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/model/Gempa.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/model/Gempa.kt @@ -43,4 +43,7 @@ class Gempa { @SerializedName("Dirasakan") @Expose var dirasakan: String? = null + + @SerializedName("Potensi") + var potensi: String? = null } \ No newline at end of file diff --git a/app/src/main/java/io/chronostech/awasgempabumi/view/DetailFragment.kt b/app/src/main/java/io/chronostech/awasgempabumi/view/DetailFragment.kt index 8e7fbfe..9234c01 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/view/DetailFragment.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/view/DetailFragment.kt @@ -2,13 +2,22 @@ package io.chronostech.awasgempabumi.view import android.Manifest import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Intent import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.net.Uri +import android.os.Build import android.os.Bundle +import android.os.Environment +import android.provider.MediaStore import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast -import androidx.core.app.ActivityCompat +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.NavHostFragment @@ -26,6 +35,11 @@ import dagger.hilt.android.AndroidEntryPoint import io.chronostech.awasgempabumi.Util.Companion.province import io.chronostech.awasgempabumi.databinding.FragmentDetailBinding import io.chronostech.awasgempabumi.viewmodel.DetailViewModel +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.* @AndroidEntryPoint class DetailFragment : Fragment(), OnMapReadyCallback { @@ -49,26 +63,63 @@ class DetailFragment : Fragment(), OnMapReadyCallback { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewModel.gempa.value?.coordinates?.let { viewModel.getPotensiTsunami(it) } + + viewModel.gempa.observe(viewLifecycleOwner, { + val coordinate = it.coordinates!!.split(",") + val latitude: Double = coordinate.get(0).toDouble() + val longitude: Double = coordinate.get(1).toDouble() + val latLng = LatLng(latitude, longitude) + + val detailPlace = it.dirasakan?.replace("I", "")?.replace("V", "")?.removePrefix("-") + ?.removePrefix(" ")?.split(", ") + province.forEach { bound -> + if (bound.value.contains(latLng)) { + binding.tvDetailPlace.text = "${detailPlace?.get(0)}, ${bound.key}" + } else { + binding.tvDetailPlace.text = "${detailPlace?.get(0)}" + } + } - val coordinate = viewModel.gempa.value?.coordinates!!.split(",") - val latitude: Double = coordinate.get(0).toDouble() - val longitude: Double = coordinate.get(1).toDouble() - val latLng = LatLng(latitude, longitude) + binding.tvMagnitude.text = String.format("%.2f", it.magnitude?.toDouble()) + binding.tvTimeGempa.text = formatTime(it.dateTime!!) + binding.tvLatlon.text = "${it.lintang}, ${it.bujur}" + binding.tvDepth.text = it.kedalaman + binding.clPotensi.visibility = View.INVISIBLE + }) - province.forEach { - if (it.value.contains(latLng)) { - binding.tvDetailPlace.text = it.key + viewModel.potensiTsunami.observe(viewLifecycleOwner, { + if (it) { + binding.clPotensi.visibility = View.VISIBLE } - } + }) - if (checkLocationPermission()) setupMapView(savedInstanceState) + if (allPermissionsGranted()) { + setupMapView(savedInstanceState) + } else { + permReqLauncher.launch(REQUIRED_PERMISSIONS) + } setupAds() + binding.btnShare.setOnClickListener { + shareAction() + } + binding.btnShare.visibility = View.GONE + binding.btnBack.setOnClickListener { NavHostFragment.findNavController(this).navigateUp() } } + private fun formatTime(dateTime: String): String { + val serverFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US) + serverFormat.timeZone = TimeZone.getTimeZone("UTC") + val date = serverFormat.parse(dateTime) + val displayFormat = SimpleDateFormat("dd MMM yyyy HH:mm", Locale.getDefault()) + displayFormat.timeZone = TimeZone.getDefault() + return displayFormat.format(date) + } + private fun setupAds() { MobileAds.initialize(requireContext()) { val adRequest = AdRequest.Builder().build() @@ -109,48 +160,80 @@ class DetailFragment : Fragment(), OnMapReadyCallback { } } - private fun checkLocationPermission(): Boolean { - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.ACCESS_FINE_LOCATION - ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION - ) != PackageManager.PERMISSION_GRANTED - ) { - requestPermissions( - arrayOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - ), LOCATION_PERMISSION_REQUEST - ) - return false - } - return true + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + requireContext(), it + ) == PackageManager.PERMISSION_GRANTED } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - LOCATION_PERMISSION_REQUEST -> { - if ((grantResults.isNotEmpty() && - grantResults[0] == PackageManager.PERMISSION_GRANTED) - ) { - setupMapView(null) - } else { - Toast.makeText( - requireContext(), - "Ijin lokasi aplikasi ditolak", - Toast.LENGTH_LONG - ).show() - } - return + private val permReqLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val granted = permissions.entries.all { + it.value } - else -> { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (granted) { + setupMapView(null) + } else { + Toast.makeText( + requireContext(), + "Permissions not granted by the user.", + Toast.LENGTH_SHORT + ).show() } } + + private fun shareAction() { + val view = binding.clFrame + val bitmap = Bitmap.createBitmap( + view.measuredWidth, + view.measuredHeight, + Bitmap.Config.ARGB_8888 + ) + + val canvas = Canvas(bitmap) + view.layout(view.left, view.top, view.right, view.bottom) + view.draw(canvas) + + val imageUri = saveImage(bitmap) + + val shareIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, imageUri) + type = "image/*" + } + startActivity(Intent.createChooser(shareIntent, "Bagikan laman ini")) + } + + private fun saveImage(bitmap: Bitmap): Uri? { + var fos: OutputStream? = null + var imageUri: Uri? = null + + val timestamp = SimpleDateFormat("yyyyMMdd_hhMMss").format(Date()) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver = requireContext().contentResolver + val contentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, timestamp) + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/PNG") + contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/" + "AwasGempaBumi") + imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + fos = resolver.openOutputStream(imageUri!!) + } else { + val imagesDir = Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DCIM + ).toString() + File.separator + "AwasGempaBumi" + + val file = File(imagesDir) + if (!file.exists()) file.mkdir() + val image = File(imagesDir, timestamp + ".png") + imageUri = Uri.fromFile(image) + fos = FileOutputStream(image) + } + + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) + fos?.flush() + fos?.close() + return imageUri } override fun onDestroyView() { @@ -161,5 +244,11 @@ class DetailFragment : Fragment(), OnMapReadyCallback { companion object { var latitude: Double? = null var longitude: Double? = null + + private val REQUIRED_PERMISSIONS = arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) } } \ No newline at end of file diff --git a/app/src/main/java/io/chronostech/awasgempabumi/view/MainFragment.kt b/app/src/main/java/io/chronostech/awasgempabumi/view/MainFragment.kt index 3992f36..c8bbfcd 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/view/MainFragment.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/view/MainFragment.kt @@ -9,7 +9,8 @@ import android.os.Bundle import android.util.Log import android.view.* import android.widget.Toast -import androidx.core.app.ActivityCompat +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.NavHostFragment @@ -68,7 +69,11 @@ class MainFragment : Fragment(), OnMapReadyCallback, GoogleMap.OnMarkerClickList override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (checkLocationPermission()) setupMapView(savedInstanceState) + if (allPermissionsGranted()) { + setupMapView(savedInstanceState) + } else { + permReqLauncher.launch(REQUIRED_PERMISSIONS) + } setupRecyclerView() viewModel.earthquakeList.observe(this, { @@ -152,7 +157,7 @@ class MainFragment : Fragment(), OnMapReadyCallback, GoogleMap.OnMarkerClickList private fun showDeviceLocation() { try { - if (checkLocationPermission()) { + if (allPermissionsGranted()) { val locationResult = fusedLocationProviderClient.lastLocation locationResult.addOnCompleteListener(requireActivity()) { task -> if (task.isSuccessful) { @@ -195,49 +200,27 @@ class MainFragment : Fragment(), OnMapReadyCallback, GoogleMap.OnMarkerClickList } } - private fun checkLocationPermission(): Boolean { - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.ACCESS_FINE_LOCATION - ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.ACCESS_COARSE_LOCATION - ) != PackageManager.PERMISSION_GRANTED - ) { - requestPermissions( - arrayOf( - Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.ACCESS_FINE_LOCATION - ), LOCATION_PERMISSION_REQUEST - ) - return false - } - return true + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + requireContext(), it + ) == PackageManager.PERMISSION_GRANTED } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - LOCATION_PERMISSION_REQUEST -> { - if ((grantResults.isNotEmpty() && - grantResults[0] == PackageManager.PERMISSION_GRANTED) - ) { - setupMapView(null) - } else { - Toast.makeText( - requireContext(), - "Ijin lokasi aplikasi ditolak", - Toast.LENGTH_LONG - ).show() - } - return + private val permReqLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val granted = permissions.entries.all { + it.value } - else -> { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (granted) { + setupMapView(null) + } else { + Toast.makeText( + requireContext(), + "Permissions not granted by the user.", + Toast.LENGTH_SHORT + ).show() } } - } override fun onMarkerClick(marker: Marker): Boolean { TODO("Not yet implemented") @@ -247,4 +230,11 @@ class MainFragment : Fragment(), OnMapReadyCallback, GoogleMap.OnMarkerClickList super.onDestroyView() _binding = null } + + companion object { + private val REQUIRED_PERMISSIONS = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION + ) + } } \ No newline at end of file diff --git a/app/src/main/java/io/chronostech/awasgempabumi/viewmodel/DetailViewModel.kt b/app/src/main/java/io/chronostech/awasgempabumi/viewmodel/DetailViewModel.kt index 4cb5168..3686130 100644 --- a/app/src/main/java/io/chronostech/awasgempabumi/viewmodel/DetailViewModel.kt +++ b/app/src/main/java/io/chronostech/awasgempabumi/viewmodel/DetailViewModel.kt @@ -2,9 +2,48 @@ package io.chronostech.awasgempabumi.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import io.chronostech.awasgempabumi.EarthQuakeRepo import io.chronostech.awasgempabumi.model.Gempa +import kotlinx.coroutines.* +import javax.inject.Inject -class DetailViewModel : ViewModel() { +@HiltViewModel +class DetailViewModel @Inject constructor(private val repo: EarthQuakeRepo) : ViewModel() { val gempa = MutableLiveData() + val potensiTsunami = MutableLiveData() + + var job: Job? = null + val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + onError("Exception handled: ${throwable.localizedMessage}") + } + val errorMessage = MutableLiveData() + + fun getPotensiTsunami(coordinates: String) { + job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch { + val response = repo.getResponseBigMagnitude() + withContext(Dispatchers.Main) { + if (response.isSuccessful) { + response.body()?.infogempa?.gempa?.forEach { + if (coordinates == it.coordinates && !it.potensi!!.contains("Tidak")) { + potensiTsunami.value = true + } + } + } else { + onError("Error : ${response.errorBody()}") + } + } + } + } + + + private fun onError(message: String) { + errorMessage.postValue(message) + } + + override fun onCleared() { + super.onCleared() + job?.cancel() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_building.xml b/app/src/main/res/drawable/ic_building.xml new file mode 100644 index 0000000..b0f94c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_building.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_danger.xml b/app/src/main/res/drawable/ic_danger.xml new file mode 100644 index 0000000..dea1a77 --- /dev/null +++ b/app/src/main/res/drawable/ic_danger.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..5b78342 --- /dev/null +++ b/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_time.xml b/app/src/main/res/drawable/ic_time.xml new file mode 100644 index 0000000..efa3620 --- /dev/null +++ b/app/src/main/res/drawable/ic_time.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4589c0f..87bdd0b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".MainActivity"> + tools:context=".view.MainActivity"> @@ -53,7 +53,7 @@ android:layout_height="wrap_content" android:background="@android:color/transparent" android:foreground="?attr/selectableItemBackground" - android:paddingHorizontal="12dp" + android:padding="12dp" android:src="@drawable/ic_share" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -66,6 +66,7 @@ @@ -78,11 +79,12 @@ android:paddingTop="16dp" android:paddingBottom="8dp" android:textAlignment="center" - android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" android:textColor="@color/white" android:textStyle="bold" + android:textSize="18sp" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="Kabupaten Jayapura, Papua" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 189defa..6bf5ea5 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context=".MainFragment"> + tools:context=".view.MainFragment">