[android] - offline test, headless region download

This commit is contained in:
tobrun
2019-04-29 15:08:17 +02:00
committed by Tobrun
parent 0fd31267f1
commit 8f90e34094
7 changed files with 554 additions and 2 deletions

View File

@@ -14,6 +14,7 @@
android:roundIcon="@drawable/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".activity.FeatureOverviewActivity"
android:label="@string/app_name"
@@ -953,7 +954,17 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.offline.DownloadRegionActivity"
android:description="@string/description_region"
android:label="@string/activity_region">
<meta-data
android:name="@string/category"
android:value="@string/category_offline" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<!-- For Instrumentation tests -->
<activity
android:name=".activity.style.RuntimeStyleTimingTestActivity"

View File

@@ -0,0 +1,313 @@
package com.mapbox.mapboxsdk.testapp.activity.offline
import android.os.Bundle
import android.os.Handler
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.ArrayAdapter
import android.widget.SeekBar
import android.widget.TextView
import android.widget.Toast
import com.mapbox.mapboxsdk.constants.MapboxConstants
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.geometry.LatLngBounds
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.offline.*
import com.mapbox.mapboxsdk.testapp.R
import kotlinx.android.synthetic.main.activity_region_download.*
import timber.log.Timber
import java.util.*
import java.util.concurrent.TimeUnit
/**
* Example showcasing how to download an offline region headless, allows to test pausing and resuming the download.
*/
class DownloadRegionActivity : AppCompatActivity(), OfflineRegion.OfflineRegionObserver {
companion object {
const val STATUS_UPDATE_TIMEOUT_MS = 10_000L
}
private val handler: Handler = Handler()
private lateinit var offlineManager: OfflineManager
private var offlineRegion: OfflineRegion? = null
private var downloading = false
private var previousCompletedResourceCount: Long = 0
private var previousUpdateTimestamp: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_region_download)
offlineManager = OfflineManager.getInstance(this)
offlineManager.setOfflineMapboxTileCountLimit(Long.MAX_VALUE)
initUi()
deleteOldOfflineRegions {
container.visibility = View.VISIBLE
fab.visibility = View.VISIBLE
}
}
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
offlineRegion?.setObserver(null)
super.onDestroy()
}
private fun createOfflineRegion() {
val latitudeNorth = editTextLatNorth.text.toString().toDouble()
val longitudeEast = editTextLonEast.text.toString().toDouble()
val latitudeSouth = editTextLatSouth.text.toString().toDouble()
val longitudeWest = editTextLonWest.text.toString().toDouble()
val styleUrl = spinnerStyleUrl.selectedItem as String
val maxZoom = seekbarMaxZoom.progress.toFloat()
val minZoom = seekbarMinZoom.progress.toFloat()
if (!validCoordinates(latitudeNorth, longitudeEast, latitudeSouth, longitudeWest)) {
Toast.makeText(this, "coordinates need to be in valid range", Toast.LENGTH_LONG).show()
return
}
// create offline definition from data
val definition = OfflineTilePyramidRegionDefinition(
styleUrl,
LatLngBounds.Builder()
.include(LatLng(latitudeNorth, longitudeEast))
.include(LatLng(latitudeSouth, longitudeWest))
.build(),
minZoom.toDouble(),
maxZoom.toDouble(),
resources.displayMetrics.density
)
logMessage("Creating offline region")
offlineManager.createOfflineRegion(definition, byteArrayOf(), object : OfflineManager.CreateOfflineRegionCallback {
override fun onCreate(region: OfflineRegion) {
logMessage("Region with id ${region.id} created")
offlineRegion = region
startDownload(region)
fab.visibility = View.VISIBLE
}
override fun onError(error: String) {
logMessage("Failed to create offline region: $error")
}
})
}
private fun startDownload(region: OfflineRegion) {
downloading = true
fab.setImageResource(R.drawable.ic_pause_black_24dp)
logMessage("Downloading...")
region.setObserver(this)
region.setDownloadState(OfflineRegion.STATE_ACTIVE)
previousUpdateTimestamp = System.currentTimeMillis()
handler.postDelayed(object : Runnable {
override fun run() {
if (System.currentTimeMillis() > previousUpdateTimestamp + STATUS_UPDATE_TIMEOUT_MS) {
logMessage("FAILURE! No progress in ${TimeUnit.MILLISECONDS.toSeconds(STATUS_UPDATE_TIMEOUT_MS)} seconds")
} else {
handler.postDelayed(this, 1000)
}
}
}, 1000)
}
private fun pauseDownload(region: OfflineRegion) {
downloading = false
fab.setImageResource(R.drawable.ic_play_arrow_black_24dp)
handler.removeCallbacksAndMessages(null)
region.setDownloadState(OfflineRegion.STATE_INACTIVE)
"Paused".let {
logMessage(it)
download_status.text = it
}
}
override fun onStatusChanged(status: OfflineRegionStatus) {
if (status.isComplete) {
val statusText = "Completed"
logMessage("SUCCESS! $statusText")
download_status.text = statusText
offlineRegion?.setObserver(null)
handler.removeCallbacksAndMessages(null)
} else {
val statusText = "Downloaded ${status.completedResourceCount}/${status.requiredResourceCount}"
Timber.d(statusText)
download_status.text = statusText
if (status.completedResourceCount > status.requiredResourceCount &&
previousCompletedResourceCount <= status.requiredResourceCount) {
logMessage("FAILURE! Completed > required")
}
}
previousCompletedResourceCount = status.completedResourceCount
previousUpdateTimestamp = System.currentTimeMillis()
}
override fun onError(error: OfflineRegionError) {
logMessage("Error: $error")
}
override fun mapboxTileCountLimitExceeded(limit: Long) {
logMessage("Error: tile count limit exceeded")
}
protected fun logMessage(message: String) {
Timber.d(message)
logView.append(message)
logView.append("\n")
}
fun deleteOldOfflineRegions(onCompleted: () -> Unit) {
offlineManager.listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback {
override fun onList(offlineRegions: Array<out OfflineRegion>) {
val count = offlineRegions.size
var remainingCount = count
if (count > 0) {
logMessage("Deleting $count old region...")
offlineRegions.forEach {
it.delete(object : OfflineRegion.OfflineRegionDeleteCallback {
override fun onDelete() {
Timber.d("Deleted region with id ${it.id}")
onProcessed()
}
override fun onError(error: String) {
Timber.e("Failed to delete region: $error")
onProcessed()
}
private fun onProcessed() {
remainingCount--
if (remainingCount == 0) {
logMessage("Done deleting")
onCompleted()
}
}
})
}
} else {
onCompleted()
}
}
override fun onError(error: String) {
logMessage("Failed to list offline regions: $error")
onCompleted()
}
})
}
// ui
private val logView: TextView by lazy {
findViewById<TextView>(R.id.log_text)
}
private fun initUi() {
initEditTexts()
initSeekbars()
initSpinner()
initZoomLevelTextviews()
initSeekbarListeners()
initFab()
}
private fun initEditTexts() {
editTextLatNorth.setText("62.0")
editTextLonEast.setText("24.0")
editTextLatSouth.setText("60.0")
editTextLonWest.setText("22.5")
}
private fun initSeekbars() {
val maxZoom = MapboxConstants.MAXIMUM_ZOOM.toInt()
seekbarMinZoom.max = maxZoom
seekbarMinZoom.progress = 1
seekbarMaxZoom.max = maxZoom
seekbarMaxZoom.progress = 15
}
private fun initSpinner() {
val styles = ArrayList<String>()
styles.add(Style.MAPBOX_STREETS)
styles.add(Style.DARK)
styles.add(Style.LIGHT)
styles.add(Style.OUTDOORS)
val spinnerArrayAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, styles)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerStyleUrl.adapter = spinnerArrayAdapter
}
private fun initZoomLevelTextviews() {
textViewMaxText.text = String.format("Max zoom: %s", seekbarMaxZoom.progress)
textViewMinText.text = String.format("Min zoom: %s", seekbarMinZoom.progress)
}
private fun initFab() {
fab.setOnClickListener {
container.visibility = View.GONE
if (offlineRegion == null) {
fab.visibility = View.GONE
createOfflineRegion()
} else {
offlineRegion?.let {
if (downloading) {
pauseDownload(it)
} else {
startDownload(it)
}
}
}
}
}
private fun initSeekbarListeners() {
seekbarMaxZoom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
textViewMaxText.text = String.format("Max zoom: %s", progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
})
seekbarMinZoom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
textViewMinText.text = String.format("Min zoom: %s", progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
})
}
private fun validCoordinates(latitudeNorth: Double, longitudeEast: Double, latitudeSouth: Double,
longitudeWest: Double): Boolean {
if (latitudeNorth < -90 || latitudeNorth > 90) {
return false
} else if (longitudeEast < -180 || longitudeEast > 180) {
return false
} else if (latitudeSouth < -90 || latitudeSouth > 90) {
return false
} else if (longitudeWest < -180 || longitudeWest > 180) {
return false
}
return true
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.NestedScrollView
android:id="@+id/container"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_marginTop="8dp"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Latitude north:"/>
<EditText
android:id="@+id/editTextLatNorth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:inputType="numberDecimal"
android:maxLines="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Longitude east:"/>
<EditText
android:id="@+id/editTextLonEast"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:inputType="numberDecimal"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingRight="2dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Latitude south:"/>
<EditText
android:id="@+id/editTextLatSouth"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:inputType="numberDecimal"
android:maxLines="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingLeft="2dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Longitude west:"/>
<EditText
android:id="@+id/editTextLonWest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:inputType="numberDecimal"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="Style url:"/>
<Spinner
android:id="@+id/spinnerStyleUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/textViewMinText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/seekbarMinZoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
<TextView
android:id="@+id/textViewMaxText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/seekbarMaxZoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/log_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textSize="8sp"
tools:text="Log text container"/>
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="64dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/primary"
android:id="@+id/bottom"
app:layout_constraintBottom_toBottomOf="parent">
<TextView
android:id="@+id/download_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="Not downloading"
android:textColor="@color/white"
android:textSize="16sp"/>
</FrameLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="@id/bottom"
android:background="@color/primary"
app:layout_constraintBottom_toBottomOf="@id/bottom"
app:layout_constraintBottom_toTopOf="@id/bottom"
android:src="@drawable/ic_play_arrow_black_24dp"
android:tint="@android:color/white"
android:layout_marginBottom="64dp"
app:backgroundTint="@color/primary"/>
</android.support.constraint.ConstraintLayout>

View File

@@ -18,6 +18,7 @@
<string name="description_map_padding">Map Padding example</string>
<string name="description_debug_mode">Debug Mode</string>
<string name="description_offline">Offline Map example</string>
<string name="description_region">Download an offline region.</string>
<string name="description_update_metadata">Update metadata example</string>
<string name="description_offline_region_delete">Delete region example</string>
<string name="description_change_resources_cache_path">Change resources cache path example</string>

View File

@@ -83,4 +83,5 @@
<string name="activity_nested_viewpager">Nested ViewPager</string>
<string name="activity_performance_measurement">Performance Measurement</string>
<string name="activity_physical_circle">Physical Unit Circle</string>
<string name="activity_region">Download region</string>
</resources>

View File

@@ -50,5 +50,6 @@
"EspressoTestActivity",
"FragmentBackStackActivity",
"ChildFragmentMapInDialogActivity",
"PerformanceMeasurementActivity"
"PerformanceMeasurementActivity",
"DownloadRegionActivity"
]