[android] - offline test, headless region download
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -50,5 +50,6 @@
|
||||
"EspressoTestActivity",
|
||||
"FragmentBackStackActivity",
|
||||
"ChildFragmentMapInDialogActivity",
|
||||
"PerformanceMeasurementActivity"
|
||||
"PerformanceMeasurementActivity",
|
||||
"DownloadRegionActivity"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user