diff options
author | Iván Ávalos <avalos@disroot.org> | 2022-01-20 01:53:52 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2022-01-20 01:53:52 -0600 |
commit | e5f4e67b85ff42cb116b99a9abb013067a5636ad (patch) | |
tree | f9524b126dcbe13ee62edf6eb6b254eb3a0740fe | |
parent | 1cc7aee5c2ae7d587d9d2e09f0fea3f546ceb27a (diff) | |
parent | 84f65df0a794dee2ee21e80f83ed9e589412acc1 (diff) | |
download | etbsa-trackermap-mobile-e5f4e67b85ff42cb116b99a9abb013067a5636ad.tar.gz etbsa-trackermap-mobile-e5f4e67b85ff42cb116b99a9abb013067a5636ad.tar.bz2 etbsa-trackermap-mobile-e5f4e67b85ff42cb116b99a9abb013067a5636ad.zip |
Merge branch 'main' of https://git.sr.ht/~avalos/trackermap-mobile
13 files changed, 351 insertions, 28 deletions
diff --git a/androidApp/src/google/AndroidManifest.xml b/androidApp/src/google/AndroidManifest.xml index f7aea6c..ce72a8b 100644 --- a/androidApp/src/google/AndroidManifest.xml +++ b/androidApp/src/google/AndroidManifest.xml @@ -7,7 +7,10 @@ <application android:name=".GoogleMainApplication" tools:replace="android:name" - tools:ignore="GoogleAppIndexingWarning"> + tools:ignore="GoogleAppIndexingWarning" + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:fullBackupContent="false"> <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" @@ -23,6 +26,16 @@ </intent-filter> </service> + <provider + android:authorities="${applicationId}.fileprovider" + android:name="androidx.core.content.FileProvider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/filepaths" /> + </provider> + </application> </manifest> diff --git a/androidApp/src/google/java/ManagerMessagingService.kt b/androidApp/src/google/java/ManagerMessagingService.kt index 07ee427..9ac4636 100644 --- a/androidApp/src/google/java/ManagerMessagingService.kt +++ b/androidApp/src/google/java/ManagerMessagingService.kt @@ -16,14 +16,13 @@ package mx.trackermap.TrackerMap.android import android.annotation.SuppressLint -import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent import android.os.Build -import androidx.core.app.NotificationCompat import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import kotlinx.coroutines.DelicateCoroutinesApi +import mx.trackermap.TrackerMap.android.shared.Utils import mx.trackermap.TrackerMap.android.units.UnitsActivity import kotlin.time.ExperimentalTime @@ -39,14 +38,14 @@ class ManagerMessagingService : FirebaseMessagingService() { } else { PendingIntent.FLAG_ONE_SHOT } - val pendingIntent = PendingIntent.getActivity(this, 0, Intent(this, UnitsActivity::class.java), flags) - val builder = NotificationCompat.Builder(this, getString(R.string.notification_channel_id)) - .setSmallIcon(R.drawable.icon_notify) - .setContentTitle(getString(R.string.app_name)) - .setContentText(remoteMessage.notification?.body) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(remoteMessage.hashCode(), builder.build()) + val intent = Intent(this, UnitsActivity::class.java) + val pendingIntent = PendingIntent.getActivity(this, 0, intent, flags) + Utils.showNotification( + this, + remoteMessage.hashCode(), + remoteMessage.notification?.body, + pendingIntent + ) } override fun onNewToken(token: String) { diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/DetailsActivity.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/DetailsActivity.kt index 56d5c4b..9633332 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/DetailsActivity.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/DetailsActivity.kt @@ -5,6 +5,7 @@ import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import com.google.android.material.tabs.TabLayout @@ -108,6 +109,13 @@ class DetailsActivity : AppCompatActivity() { } ActivityCompat.shouldShowRequestPermissionRationale(this, permission) -> { Log.d("DetailsActivity", "shouldShowRequestPermissionRationale") + AlertDialog.Builder(this) + .setTitle(R.string.write_rationale_title) + .setMessage(R.string.write_rationale_msg) + .setPositiveButton(R.string.shared_ok) { _, _ -> + launcher.launch(permission) + } + .create().show() } else -> { Log.d("DetailsActivity", "Requesting $permission permission") diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt index 51d2f6d..08d7076 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsFragment.kt @@ -22,12 +22,14 @@ import mx.trackermap.TrackerMap.android.R import mx.trackermap.TrackerMap.android.databinding.UnitDetailsReportsBinding import mx.trackermap.TrackerMap.android.details.UnitDetailsAdapter import mx.trackermap.TrackerMap.android.map.MapWrapperFragment +import mx.trackermap.TrackerMap.android.shared.FileCache +import mx.trackermap.TrackerMap.android.shared.Utils import mx.trackermap.TrackerMap.client.models.EventInformation import mx.trackermap.TrackerMap.controllers.ReportController import mx.trackermap.TrackerMap.utils.Formatter import mx.trackermap.TrackerMap.utils.ReportDates import org.koin.androidx.viewmodel.ext.android.viewModel -import java.lang.Exception +import java.io.* import kotlin.math.max import kotlin.time.ExperimentalTime @@ -41,6 +43,13 @@ class UnitReportsFragment : Fragment() { private val unitReportsViewModel: UnitReportsViewModel by viewModel() private lateinit var mapFragment: MapWrapperFragment + private var reportFile: ReportController.Report.XlsxReport? = null + private var exportAction: UnitReportsViewModel.ExportAction? = null + + private companion object { + const val XLSX_MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -104,6 +113,11 @@ class UnitReportsFragment : Fragment() { showPeriodPopUp(it) } binding.exportButton.setOnClickListener { + exportAction = UnitReportsViewModel.ExportAction.ACTION_SAVE + unitReportsViewModel.fetchReportXlsx() + } + binding.shareButton.setOnClickListener { + exportAction = UnitReportsViewModel.ExportAction.ACTION_SHARE unitReportsViewModel.fetchReportXlsx() } unitReportsViewModel.setReportPeriod(ReportDates.ReportPeriod.TODAY) @@ -131,7 +145,12 @@ class UnitReportsFragment : Fragment() { showMap(true) } is ReportController.Report.XlsxReport -> { - downloadFile("report.xlsx") + reportFile = report + when (exportAction) { + UnitReportsViewModel.ExportAction.ACTION_SHARE -> shareFile() + UnitReportsViewModel.ExportAction.ACTION_SAVE -> saveFile() + else -> {} + } } is ReportController.Report.LoadingReport -> loading() } @@ -280,12 +299,12 @@ class UnitReportsFragment : Fragment() { } } - private fun downloadFile(filename: String) { + private fun saveFile(filename: String = "reports.xlsx") { val filesDir = requireContext().getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) filesDir?.let { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) - type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + type = XLSX_MIME_TYPE putExtra(Intent.EXTRA_TITLE, filename) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { putExtra(DocumentsContract.EXTRA_INITIAL_URI, filesDir) @@ -295,10 +314,19 @@ class UnitReportsFragment : Fragment() { } } + private fun shareFile(filename: String = "reports.xlsx") { + context?.let { context -> + reportFile?.data?.let { data -> + val cacheFile = Utils.saveReportToCache(context, data, filename) + Utils.shareFile(context, cacheFile, XLSX_MIME_TYPE) + } + } + } + private val createDocumentResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == Activity.RESULT_OK) { - (unitReportsViewModel.report.value as? ReportController.Report.XlsxReport)?.let { report -> + reportFile?.let { report -> it?.data?.data?.also { uri -> Log.d("UnitReportsFragment", "Downloading file into ${uri.path}") val outputStream = context?.contentResolver?.openOutputStream(uri) @@ -308,7 +336,23 @@ class UnitReportsFragment : Fragment() { outputStream.flush() outputStream.close() Log.d("UnitReportsFragment", "Wrote file into ${uri.path}") - } catch (e: Exception) { + + activity?.let { context -> + // Copy file to cache so we can open it + val fc = FileCache(context) + fc.removeAll() + fc.cacheThis(listOf(uri)) + + val dir = File(context.cacheDir, "reports_tmp") + val files = dir.listFiles() + if (files?.isNotEmpty() == true) { + val intent = + Utils.openFileIntent(context, files[0], XLSX_MIME_TYPE) + openFileResult.launch(intent) + } + } + + } catch (e: IOException) { e.printStackTrace() } } @@ -316,4 +360,11 @@ class UnitReportsFragment : Fragment() { } } } + + private val openFileResult = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + Log.d("UnitReportsFragment", "Opening file!") + } + } }
\ No newline at end of file diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsViewModel.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsViewModel.kt index 7d1e028..0bdf61b 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsViewModel.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/details/reports/UnitReportsViewModel.kt @@ -17,6 +17,10 @@ class UnitReportsViewModel( savedStateHandle: SavedStateHandle ) : ViewModel(), KoinComponent { + enum class ExportAction { + ACTION_SAVE, ACTION_SHARE + } + private val geofencesController: GeofencesController by inject() private val reportController: ReportController by inject() diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt index 37956c1..c483d52 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/map/MapFragment.kt @@ -240,7 +240,7 @@ open class MapFragment : GlobeMapFragment() { /* Draw polyline for report */ if (isReport && markers.isNotEmpty()) { val vector = VectorObject() - vector.addAreal(points) + vector.addLinear(points) objects.add(mapControl.addVector( vector, diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/FileCache.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/FileCache.kt new file mode 100644 index 0000000..16777de --- /dev/null +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/FileCache.kt @@ -0,0 +1,120 @@ +package mx.trackermap.TrackerMap.android.shared + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream + +// Source: https://techenum.com/android-content-uri-how-to-get-the-file-uri/ +class FileCache( + private val context: Context +) { + + // content resolver + private val contentResolver = context.contentResolver + + // to get the type of file + private val mimeTypeMap = MimeTypeMap.getSingleton() + + private val mCacheLocation = File(context.cacheDir, "reports_tmp") + + fun cacheThis(uris: List<Uri>) { + uris.forEach { uri -> copyFromSource(uri) } + } + + /** + * Copies the actual data from provided content provider. + */ + private fun copyFromSource(uri: Uri) { + + val fileExtension: String = getFileExtension(uri) ?: kotlin.run { + throw RuntimeException("Extension is null for $uri") + } + val fileName = queryName(uri) ?: getFileName(fileExtension) + + val inputStream = contentResolver.openInputStream(uri) ?: kotlin.run { + throw RuntimeException("Cannot open for reading $uri") + } + val bufferedInputStream = BufferedInputStream(inputStream) + + // the file which will be the new cached file + val outputFile = File(mCacheLocation, fileName) + if (!outputFile.exists()) { + outputFile.parentFile?.mkdirs() + } else { + outputFile.delete() + } + val bufferedOutputStream = BufferedOutputStream(FileOutputStream(outputFile)) + + // this will hold the content for each iteration + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + + var readBytes: Int // will be -1 if reached the end of file + + while (true) { + readBytes = bufferedInputStream.read(buffer) + + // check if the read was failure + if (readBytes == -1) { + bufferedOutputStream.flush() + break + } + + bufferedOutputStream.write(buffer) + bufferedOutputStream.flush() + } + + // close everything + inputStream.close() + bufferedInputStream.close() + bufferedOutputStream.close() + + } + + private fun getFileExtension(uri: Uri): String? { + return mimeTypeMap.getExtensionFromMimeType(contentResolver.getType(uri)) + } + + /** + * Tries to get actual name of the file being copied. + * This might be required in some of the cases where you might want to know the file name too. + * + * @param uri + * + */ + private fun queryName(uri: Uri): String? { + val returnCursor = contentResolver.query(uri, null, null, null, null) ?: return null + val nameIndex: Int = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + returnCursor.moveToFirst() + val name: String = returnCursor.getString(nameIndex) + returnCursor.close() + return name + } + + private fun getFileName(fileExtension: String): String { + return "${System.currentTimeMillis()}.$fileExtension" + } + + /** + * Remove everything that we have cached. + * You might want to invoke this method before quiting the application. + */ + fun removeAll() { + mCacheLocation.deleteRecursively() + } + + companion object { + + // base buffer size + private const val BASE_BUFFER_SIZE = 1024 + + // if you want to modify size use binary multiplier 2, 4, 6, 8 + private const val DEFAULT_BUFFER_SIZE = BASE_BUFFER_SIZE * 4 + + } + +}
\ No newline at end of file diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/Utils.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/Utils.kt index 004bcaa..abfe1bb 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/Utils.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/shared/Utils.kt @@ -1,10 +1,22 @@ package mx.trackermap.TrackerMap.android.shared +import android.app.NotificationManager +import android.app.PendingIntent import android.content.Context +import android.content.Intent +import android.util.Log import android.view.View import androidx.appcompat.widget.PopupMenu +import androidx.core.app.NotificationCompat +import androidx.core.app.ShareCompat +import androidx.core.content.FileProvider +import com.google.firebase.messaging.FirebaseMessagingService import mx.trackermap.TrackerMap.android.R import mx.trackermap.TrackerMap.client.models.MapLayer +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException class Utils { companion object { @@ -23,5 +35,64 @@ class Utils { } popOver.show() } + + fun openFileIntent(context: Context, file: File, mime: String): Intent { + Log.d("Utils", "Filename is ${file.name}") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + val intent = Intent() + intent.action = Intent.ACTION_VIEW + intent.setDataAndType(uri, mime) + intent.putExtra(Intent.EXTRA_STREAM, uri) + return intent + } + + fun shareFile(context: Context, file: File, mime: String) { + Log.d("Utils", "Filename is ${file.name}") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + ShareCompat.IntentBuilder(context) + .setType(mime) + .setStream(uri) + .setChooserTitle("Share this with your friends!") + .startChooser() + } + + fun saveReportToCache( + context: Context, + data: ByteArray, + filename: String + ): File { + val reportsDir = File(context.cacheDir, "reports_tmp") + reportsDir.deleteRecursively() + val cacheFile = File(reportsDir, filename) + if (!cacheFile.exists()) { + cacheFile.parentFile?.mkdirs() + } else { + cacheFile.delete() + } + + val output = BufferedOutputStream(FileOutputStream(cacheFile)) + + try { + output.write(data) + output.flush() + output.close() + } catch (e: IOException) { + e.printStackTrace() + } + + return cacheFile + } + + fun showNotification(context: Context, id: Int, body: String?, pendingIntent: PendingIntent) { + val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_id)) + .setSmallIcon(R.drawable.icon_notify) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText(body) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + (context.getSystemService(FirebaseMessagingService.NOTIFICATION_SERVICE) + as NotificationManager) + .notify(id, builder.build()) + } } }
\ No newline at end of file diff --git a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt index 9fcef7f..58147e4 100644 --- a/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt +++ b/androidApp/src/main/java/mx/trackermap/TrackerMap/android/units/UnitsActivity.kt @@ -66,6 +66,10 @@ class UnitsActivity : AppCompatActivity() { } private fun setupViews() { + supportFragmentManager.commit { + add(R.id.displayContainer, mapFragment) + add(R.id.displayContainer, devicesFragment) + } TooltipCompat.setTooltipText(binding.userButton, getString(R.string.open_profile)) TooltipCompat.setTooltipText(binding.mapLayerToggle, getString(R.string.switch_layer)) } @@ -148,9 +152,15 @@ class UnitsActivity : AppCompatActivity() { UnitsViewModel.UnitsDisplayMode.MAP -> mapFragment else -> mapFragment } + val oldFragment = + when (displayMode) { + UnitsViewModel.UnitsDisplayMode.LIST -> mapFragment + UnitsViewModel.UnitsDisplayMode.MAP -> devicesFragment + else -> devicesFragment + } supportFragmentManager.commit { - replace(R.id.displayContainer, newFragment) - setTransition(TRANSIT_FRAGMENT_FADE) + show(newFragment) + hide(oldFragment) } } unitsViewModel.selectedUnit.observe(this) { diff --git a/androidApp/src/main/res/layout/unit_details_reports.xml b/androidApp/src/main/res/layout/unit_details_reports.xml index f53ce51..903c2c3 100644 --- a/androidApp/src/main/res/layout/unit_details_reports.xml +++ b/androidApp/src/main/res/layout/unit_details_reports.xml @@ -106,12 +106,6 @@ android:textColor="@color/colorPrimaryDark" app:backgroundTint="@color/darkBackground" /> - <com.google.android.material.button.MaterialButton - android:id="@+id/exportButton" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/export_report" /> - </LinearLayout> <com.addisonelliott.segmentedbutton.SegmentedButtonGroup @@ -159,6 +153,31 @@ </com.addisonelliott.segmentedbutton.SegmentedButtonGroup> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginTop="@dimen/margin"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/exportButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/export_report" + android:layout_weight="1" + android:layout_marginEnd="@dimen/fields_spacing" + style="?android:buttonStyleSmall"/> + + <com.google.android.material.button.MaterialButton + android:id="@+id/shareButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/share_report" + style="?android:buttonStyleSmall"/> + + </LinearLayout> + </LinearLayout> <include diff --git a/androidApp/src/main/res/values-es-rMX/strings.xml b/androidApp/src/main/res/values-es-rMX/strings.xml index c9bc820..28aee79 100644 --- a/androidApp/src/main/res/values-es-rMX/strings.xml +++ b/androidApp/src/main/res/values-es-rMX/strings.xml @@ -89,7 +89,19 @@ <string name="stops">Paradas</string> <string name="period">Periodo</string> <string name="select_period">Seleccionar</string> - <string name="export_report">Exportar</string> + + <string name="export_report">Guardar</string> + <string name="share_report">Compartir</string> + <string name="export_stored">El reporte ha sido descargado</string> + <string name="export_open_file_with">Abrir con</string> + + <string name="write_rationale_title"> + Necesitamos tu permiso + </string> + <string name="write_rationale_msg"> + Esta aplicación requiere acceso de escritura a tus archivos. + Requerimos este permiso para guardar reportes en tu teléfono. + </string> <!-- Report periods --> <string name="period_today">Hoy</string> diff --git a/androidApp/src/main/res/values/strings.xml b/androidApp/src/main/res/values/strings.xml index 0e947be..4f5a213 100644 --- a/androidApp/src/main/res/values/strings.xml +++ b/androidApp/src/main/res/values/strings.xml @@ -102,7 +102,19 @@ <string name="stops">Stops</string> <string name="period">Period</string> <string name="select_period">Select</string> - <string name="export_report">Export</string> + + <string name="export_report">Save</string> + <string name="share_report">Share</string> + <string name="export_stored">Report has been downloaded</string> + <string name="export_open_file_with">Open file with</string> + + <string name="write_rationale_title"> + We need your permission + </string> + <string name="write_rationale_msg"> + This app relies on write access to your files. + We require access to this permission to save reports into your phone. + </string> <!-- Report periods --> <string name="period_today">Today</string> diff --git a/androidApp/src/main/res/xml/filepaths.xml b/androidApp/src/main/res/xml/filepaths.xml new file mode 100644 index 0000000..f894ef4 --- /dev/null +++ b/androidApp/src/main/res/xml/filepaths.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths> + <cache-path name="/" path="." /> +</paths> |