From 4148a5a91aefef20f28e520d1c0d4b6485cf0234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Paczos?= Date: Mon, 10 Sep 2018 20:59:49 +0200 Subject: [PATCH] [android] expose offline database merge API --- .../mapboxsdk/offline/OfflineManager.java | 167 ++++++++++++++++++ .../testapp/offline/OfflineManagerTest.kt | 119 +++++++++++++ .../src/main/AndroidManifest.xml | 12 ++ .../src/main/assets/offline.db | Bin 0 -> 73728 bytes .../offline/MergeOfflineRegionsActivity.kt | 129 ++++++++++++++ .../main/res/layout/activity_map_simple.xml | 7 +- .../layout/activity_merge_offline_regions.xml | 20 +++ .../src/main/res/values/descriptions.xml | 1 + .../src/main/res/values/titles.xml | 1 + .../android/scripts/exclude-activity-gen.json | 3 +- .../android/src/offline/offline_manager.cpp | 72 +++++++- .../android/src/offline/offline_manager.hpp | 22 ++- 12 files changed, 538 insertions(+), 15 deletions(-) create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/assets/offline.db create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/MergeOfflineRegionsActivity.kt create mode 100644 platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_merge_offline_regions.xml diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java index 08b58fa79..fbbdf087b 100644 --- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java @@ -2,10 +2,12 @@ package com.mapbox.mapboxsdk.offline; import android.annotation.SuppressLint; import android.content.Context; +import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.support.annotation.Keep; import android.support.annotation.NonNull; + import com.mapbox.mapboxsdk.LibraryLoader; import com.mapbox.mapboxsdk.MapStrictMode; import com.mapbox.mapboxsdk.R; @@ -15,6 +17,11 @@ import com.mapbox.mapboxsdk.net.ConnectivityReceiver; import com.mapbox.mapboxsdk.storage.FileSource; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.nio.channels.FileChannel; /** * The offline manager is the main entry point for offline-related functionality. @@ -92,6 +99,27 @@ public class OfflineManager { void onError(String error); } + /** + * This callback receives an asynchronous response containing a list of all + * OfflineRegion added to the database during the merge. + */ + @Keep + public interface MergeOfflineRegionsCallback { + /** + * Receives the list of merged offline regions. + * + * @param offlineRegions the offline region array + */ + void onMerge(OfflineRegion[] offlineRegions); + + /** + * Receives the error message. + * + * @param error the error message + */ + void onError(String error); + } + /* * Constructor */ @@ -183,6 +211,143 @@ public class OfflineManager { }); } + /** + * Merge offline regions from a secondary database into the main offline database. + *

+ * When the merge is completed, or fails, the {@link MergeOfflineRegionsCallback} will be invoked on the main thread. + *

+ * The secondary database may need to be upgraded to the latest schema. + * This is done in-place and requires write-access to the provided path. + * If the app's process doesn't have write-access to the provided path, + * the file will be copied to the temporary, internal directory for the duration of the merge. + *

+ * Only resources and tiles that belong to a region will be copied over. Identical + * regions will be flattened into a single new region in the main database. + *

+ * The operation will be aborted and {@link MergeOfflineRegionsCallback#onError(String)} with an appropriate message + * will be invoked if the merge would result in the offline tile count limit being exceeded. + *

+ * Merged regions may not be in a completed status if the secondary database + * does not contain all the tiles or resources required by the region definition. + * + * @param path secondary database writable path + * @param callback completion/error callback + */ + public void mergeOfflineRegions(@NonNull String path, @NonNull final MergeOfflineRegionsCallback callback) { + File src = new File(path); + if (!src.canRead()) { + // path not readable, abort + callback.onError("Secondary database needs to be located in a readable path."); + return; + } + + if (src.canWrite()) { + // path writable, merge and update schema in place if necessary + mergeOfflineDatabaseFiles(src, callback, false); + } else { + // path not writable, copy the the file to temp directory, then merge and update schema on a copy if necessary + File dst = new File(FileSource.getInternalCachePath(context), src.getName()); + new CopyTempDatabaseFileTask(this, callback).execute(src, dst); + } + } + + private static final class CopyTempDatabaseFileTask extends AsyncTask { + private final WeakReference offlineManagerWeakReference; + private final WeakReference callbackWeakReference; + + CopyTempDatabaseFileTask(OfflineManager offlineManager, MergeOfflineRegionsCallback callback) { + this.offlineManagerWeakReference = new WeakReference<>(offlineManager); + this.callbackWeakReference = new WeakReference<>(callback); + } + + @Override + protected Object doInBackground(Object... objects) { + File src = (File) objects[0]; + File dst = (File) objects[1]; + + try { + copyTempDatabaseFile(src, dst); + return dst; + } catch (IOException ex) { + return ex.getMessage(); + } + } + + @Override + protected void onPostExecute(Object object) { + MergeOfflineRegionsCallback callback = callbackWeakReference.get(); + if (callback != null) { + OfflineManager offlineManager = offlineManagerWeakReference.get(); + if (object instanceof File && offlineManager != null) { + // successfully copied the file, perform merge + File dst = (File) object; + offlineManager.mergeOfflineDatabaseFiles(dst, callback, true); + } else if (object instanceof String) { + // error occurred + callback.onError((String) object); + } + } + } + } + + private static void copyTempDatabaseFile(File sourceFile, File destFile) throws IOException { + if (!destFile.exists() && !destFile.createNewFile()) { + throw new IOException("Unable to copy database file for merge."); + } + + FileChannel source = null; + FileChannel destination = null; + + try { + source = new FileInputStream(sourceFile).getChannel(); + destination = new FileOutputStream(destFile).getChannel(); + destination.transferFrom(source, 0, source.size()); + } catch (IOException ex) { + throw new IOException(String.format("Unable to copy database file for merge. %s", ex.getMessage())); + } finally { + if (source != null) { + source.close(); + } + if (destination != null) { + destination.close(); + } + } + } + + private void mergeOfflineDatabaseFiles(@NonNull File file, @NonNull final MergeOfflineRegionsCallback callback, + boolean isTemporaryFile) { + fileSource.activate(); + mergeOfflineRegions(fileSource, file.getAbsolutePath(), new MergeOfflineRegionsCallback() { + @Override + public void onMerge(OfflineRegion[] offlineRegions) { + getHandler().post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (isTemporaryFile) { + file.delete(); + } + callback.onMerge(offlineRegions); + } + }); + } + + @Override + public void onError(String error) { + getHandler().post(new Runnable() { + @Override + public void run() { + fileSource.deactivate(); + if (isTemporaryFile) { + file.delete(); + } + callback.onError(error); + } + }); + } + }); + } + /** * Create an offline region in the database. *

@@ -272,4 +437,6 @@ public class OfflineManager { private native void createOfflineRegion(FileSource fileSource, OfflineRegionDefinition definition, byte[] metadata, CreateOfflineRegionCallback callback); + @Keep + private native void mergeOfflineRegions(FileSource fileSource, String path, MergeOfflineRegionsCallback callback); } diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt new file mode 100644 index 000000000..dd22d28f8 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/androidTest/java/com/mapbox/mapboxsdk/testapp/offline/OfflineManagerTest.kt @@ -0,0 +1,119 @@ +package com.mapbox.mapboxsdk.testapp.offline + +import android.R +import android.content.Context +import android.support.test.espresso.Espresso.onView +import android.support.test.espresso.IdlingRegistry +import android.support.test.espresso.UiController +import android.support.test.espresso.assertion.ViewAssertions.matches +import android.support.test.espresso.idling.CountingIdlingResource +import android.support.test.espresso.matcher.ViewMatchers.isDisplayed +import android.support.test.espresso.matcher.ViewMatchers.withId +import android.support.test.runner.AndroidJUnit4 +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.offline.OfflineManager +import com.mapbox.mapboxsdk.offline.OfflineRegion +import com.mapbox.mapboxsdk.testapp.action.MapboxMapAction.invoke +import com.mapbox.mapboxsdk.testapp.activity.BaseActivityTest +import com.mapbox.mapboxsdk.testapp.activity.espresso.EspressoTestActivity +import org.junit.Test +import org.junit.runner.RunWith +import java.io.File +import java.io.FileOutputStream + +@RunWith(AndroidJUnit4::class) +class OfflineManagerTest : BaseActivityTest() { + + companion object { + private const val TEST_DB_FILE_NAME = "offline.db" + } + + private val context: Context by lazy { rule.activity } + + private lateinit var offlineIdlingResource: CountingIdlingResource + + override fun getActivityClass(): Class<*> { + return EspressoTestActivity::class.java + } + + override fun beforeTest() { + super.beforeTest() + offlineIdlingResource = CountingIdlingResource("idling_resource") + IdlingRegistry.getInstance().register(offlineIdlingResource) + } + + @Test + fun offlineMergeListDeleteTest() { + validateTestSetup() + + invoke(mapboxMap) { _: UiController, _: MapboxMap -> + offlineIdlingResource.increment() + copyAsset(context) + OfflineManager.getInstance(context).mergeOfflineRegions( + context.filesDir.absolutePath + "/" + TEST_DB_FILE_NAME, + object : OfflineManager.MergeOfflineRegionsCallback { + override fun onMerge(offlineRegions: Array?) { + assert(offlineRegions?.size == 1) + offlineIdlingResource.decrement() + } + + override fun onError(error: String?) { + throw RuntimeException("Unable to merge external offline database. $error") + } + }) + } + + invoke(mapboxMap) { _: UiController, _: MapboxMap -> + offlineIdlingResource.increment() + OfflineManager.getInstance(context).listOfflineRegions(object : OfflineManager.ListOfflineRegionsCallback { + override fun onList(offlineRegions: Array?) { + assert(offlineRegions?.size == 1) + if (offlineRegions != null) { + for (region in offlineRegions) { + offlineIdlingResource.increment() + region.delete(object : OfflineRegion.OfflineRegionDeleteCallback { + override fun onDelete() { + offlineIdlingResource.decrement() + } + + override fun onError(error: String?) { + throw RuntimeException("Unable to delete region with ID: ${region.id}. $error") + } + }) + } + } else { + throw RuntimeException("Unable to find merged region.") + } + offlineIdlingResource.decrement() + } + + override fun onError(error: String?) { + throw RuntimeException("Unable to obtain offline regions list. $error") + } + }) + } + + // waiting for offline idling resource + onView(withId(R.id.content)).check(matches(isDisplayed())) + } + + override fun afterTest() { + super.afterTest() + IdlingRegistry.getInstance().unregister(offlineIdlingResource) + } + + private fun copyAsset(context: Context) { + val bufferSize = 1024 + val assetManager = context.assets + val inputStream = assetManager.open(TEST_DB_FILE_NAME) + val outputStream = FileOutputStream(File(context.filesDir.absoluteFile, TEST_DB_FILE_NAME)) + + try { + inputStream.copyTo(outputStream, bufferSize) + } finally { + inputStream.close() + outputStream.flush() + outputStream.close() + } + } +} \ No newline at end of file diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml index a0594d8b8..5fcbcb963 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.mapbox.mapboxsdk.testapp"> + + + + + =e@nWmx~r@Is%}eP4*B?m`%Czr%_2e26MjZPyMjQ4&xb%YLF-1venNq=jU0)j zg)ECGhD3_6iI9kZ{+I|%fB+x>2mk_r03h%m1k6z3aak!KeW1;4jEr3!j7`mLZFHT? zt&AOiz3Q49{WT6vSW!e!Sp*~@`$|L|^w*T%OC!n2{#`r>{4=x1Y^Z%;P~dSz=pcQ_ z{<sXNPlFnEGQ%`@*ipd3G_R{+~~3D zltsiu6hX3b${<-4X=#w6h^UC7h^(-P(jQAXf*+YckGuLxL|WvrvV{eegau!T&^~7K zbLSrs|6eot1@tqEpDU=yN+_s^{H%dr4a?k!_IDoCNYv~|aJW(wkPyG<9POU#oCf|btKWz1U-4)`f5D;!xzmDNX+ds}O_LfH4wo1AKWm!5 zs!o^b*BhKXG#u`W7mymiD*KNk-qG0J+1Tds4E(nf!0#Z;-!*{$kO;QXw>GAIT$I}P zH6%PPF)?HS{2$Hwk2C0>4Ik?7^7%9Rf4edLV;lPUr{o_zf13YSulmQjWrBjk6(Ro5 z>h@2~pXZxD;GrKsU?BYbkb&~}ghX|Lczgg8AOHve0)PM@00;mAfB+x>2mk_r03h(M z7Vv|Ed;+0_2m_(yZ-fYgV6AUwVC%}oz~JcQ_IOd7;csCE$H!|RRzEK|(!Jr-auJJ77*+hwH`Sj& zTEFORF2hDfxp+$oUVU@YZE8|geWq!_4X!IoP5NZeXY#q|&j=ywFk|E%>0#~u7Y0-dB9ZTN+ zNdV=^j|}I@_zjy8=}ej&OFVDLr}I6PS}wv0KX39wqRm1)K7a`j00aO5KmZT`1ONd* z01yBK00BS%5C8=Js|ip-D^}?=Bs2lyAKxkZHU1xE8{+W+On?9& z00;mAfB+x>2mk_r03ZMe00RFV1lD07(IGq#5fKnzU=)0MX#P6t{#PgQk3skJe-655 z@NoBJ@Nj*6x_Ns1`tb!%dKY5@J1v(z49(0m2vRn76BB)XLrxBMRu%(BPGbXBR#tuX zmn=r?h9*p`2F#Cx@zd*!m9bxAUve|HjZ%P^oD!Y<;+$N_P&yf%g5usSk5Y(BxVV7D zpDbuY2`4%Q-`q6qhBI7tHeY&AUsqjnS^k*TFzvl=ymLVo_*nEY8$m!25<>ogu5F3+ z9S;OF3K29B3N$nkG)zZZ7iCvZPe%u3J5?LSTL~fAhZz5`u$YwOgqRQvX;IN$k=`#I z{lo3Df&)VXLp=k7J;N7EBw!Fa0s+=7EZYS#n!_kssNb~RsLpd zw`-Omiop?R1x@b3w+}osDpthp*iNpu_RJ?h<{~D|HzqkCK0IAmUR?c^vh?eRaNq2h zijbm=7N4x>^5DXf^k%;ttghLCx!#!(eh}k{r?S&v(AT=S$*QxkK{<`t0FiErJmc>; z@!>KQUfoF=eWNtG~fGm=By1A@21W*-Ujs+{HS|a zyIZ>PUMIcj4G0N*Y}V+18~@)9f!6-tp^*do00;mAfB+x>2mk_r03ZMe00MvjAOHyb zHx=N6|8-I4e~k72|8_Cw*ZqGqGl<6rFaZL903ZMe00MvjAOHve0)PM@00;mAfWZHu zfDaBNEQBu|ga_%bpE%Tg{GSb^T%`XCU(hF|B{i^hwlQ)f<2mk_rz`w5maR2|`w{C!v0Rn&kAOHve0)PM@00;mAfB+x>2mk{ADuG}3 z|Nl1r{|W-_>R**DkPjdL2mk_r03ZMe00MvjAOHve0)PM@00{iM2*5+bAZob?D**lf zf0r5o$^i%f0)PM@00;mAfB+x>2mk_r03ZMe{Obk&zW;v*fp+(=-v*E)AOHve0)PM@ z00;mAfB+x>2mk_r03ZMe{NERV{`2?$f&Tyh{n`LZ00;mAfB+x>2mk_r03ZMe00Mvj zAOHyb|0D3T|NodX2HY#?``!TGoy$yU416wLVz}~8s9(OV!9sg;PzJv~Netwp6@^?t zRwNjCX2nd|ZoM(^WAuS)_6PUp_dj%ve!%bja43PY^8x>$M-zvMg5%lPGAbT<-bHi_ ziIX%N^c5ke6X)X#yID%{DoA|tL(>Y#Z&BAe;)jGGO3@$K7C>({uBB0)t7zuI2 z=Bf$Ivc#@Wz!2r-E6r*grwGAd8YCfNeW&aURv9z+azV8ne@Le`JHm}lH)Sg=*~vBq zmj<4DjuEwm2|t9vRbjF#G|m;`BO?})wJPhY!pu=C;(JWPyhYB%%;AR45Z=Ma$oHyd zvdk*tGk8upuvsmV(K=MyWONg45%yG9z{+`>$L1dC)veaIW}A$44tj#>{sZn*yjz-V zeQeeNU08Jd?ClM#ns0i?!vU;MXI4KE>Lhl_qPNk=Ni&1#?ZMau*X1`_p_ecN)fQ4U z&Q%aksm<|8is&p4`b+7qgZmh~15sm3zLSn`W8qgE)Z^dM3&-PkEG2qcN`MiD3~Uo3 z)?i6&h}>aSo9j{wx5{A7wcXO{htD3WRis-`));uuF2r6@zvC>O9RI3(t`~L|Gqjes zK9$q0;YfFL7*N(kYZ>CpguAOpZ9yy-cPX|&>VIr%u%(mkUMWe1SW?ET9{4e1Nn%VO z%Qm&;A{!&`J&nwPJ*#`jbmobEy9K4drVi0uN20uPAVf25jWh;mzP7 z#t%jr**4#;sPdxMJO)fx1G4GC@pz86>2AbecL8Zmwn4>{nvUK{bkoG z_a3nec5)|eUxvzt=y3V@67LVjwa$3QR0=SPpN$#3VNYFd<5(D)?LLpo@)CU#h)LTV z&h~Ur+NTdZ7uqC0>k{8i=&*^y@g0XB6plxzJYZL-@;a$r$78&chspDs^cjrxxMqX;)19)wgHmEuGERKRref#?lBC4NJ z+}@-yj)}e_cQpznO+cn8Gw`-eua{tKwhk77K$Up&*8mstDh>pauwS2j+ZTk7Q%7nE*Da7X+DD<5H3 zYIfaEq=Y%8A-ZupG8l8x*UXT!a?lK!ZUkwr z)Ijy;f;r7&O5frSAQZ+V7?hUyKOBs>Lv8GC(b0G|)w9AAw&HdRaDHjHm235Uvd6i^ zRLw18x8Y!>elVDf*Gf>#>=B0{9RVriR`M-qdxGEP35E_{w+7NkD^2Je{I^3?#iyuq zw_E+K@4?VxB<(f)Eg&SLlfl^&_9|FeX|`<>BwTG(T}=v6^aXu8f0%h0qV-~v5!Yu$ zE;n9jtzW2S38^{QUK8qCILYceas@EbXvykl8W#n$&@9 zOi|WEx)~iaQ|oM!=@FaeRCNd9y+Dd8bG$v*Nn@xT@Sb27p&+Iq8f%oVD8*bVBWESgC? zPIKoe)9$PmhuU0fQzlsy8K)fuj(r>+#;`5m*TyPtLxSxZ4n(k+ZQfcgR1yqG5Tml3RS%2A7iI=uuwpekE&C9-c%({T>*|^{ zoXs4pk(D)b(h|+g!Yll~u$nzV@4xU-M0h3Q(qPf$jdBl*KJf!n;;DM-yy1nEAxG?O zyS&c~X7j|kpi_)(`Nc|V3_*l9OBHl;g~U}Tq}T`?`xC(zJa8*1`@HO*Ol=7U6T<`xy(ErZhnn*}<=5M}Q=T2`r=570 zqh%otsq^bP<9>-f66w)?fc@l)?sLez-zVKRUv)puv2ylGY;qLJt;Y%7b)os&-jlw2 zJ$PQaQ^~FD6O0Fk&ruVXqLAX(gb}(B=CV^+%V7#5dRn`bG9=SeH?J0Ollo5Pd3&2F zQ-c6^_EZ@$X%cA?R#G{?tf3tE=^Sr@Yig>{(noLPsgnG#ku(X_U9; zr5UWLUz81adc#)-=VZWG+`6Ge^}WyQ-^Q;Pw%1ZzMvG;drm#)cXNuFw&9gpdj(Rr^thYSo$M1o=vS?L{+=_*^&8ze+8SN`Ce?Amd1?{RS?7g+;@m0dIqEyii zimiso35UT^o|W}jAy7bw%CkvwYo` zx9W#HEnhg$=RZ;EeUxlrd!Kf{`fZ%>g|32CZ7ei(5#)H&tDy%v&6*z?8Sgs?U4}Iz zQnVyolvP)BD|`58K4cevq4Zl;50kAjZ?u(1=kck>H!N=`M#UKPv}9^D3pdy<1Q(9@ zTKn~qe=X@7agVI-PuK}~n$%RyY)tztO=&LoMbB&bLsogx70yPxnZs(^922n-!Kv$z2iGs=26}EX*XFP1|my zZ4Rv2Jdj&}{33kWPqjB4$8sd7H7Ks3#bhjl`~$?rOo7xk)K?Z&)H}*CB;K*Ihe1@~ z*pTX~An@aY5#8oA&z3K+I9%1gm@SJWsoDrDW#NWe#LwO0YB(%<05#lQ!xSd!^VpWY z;Anw}`&B&8MD8}`p(S6AF9EbKfuMIDI7cf%O|^mSn~M?RiwX{=@B{mmiYBgybV@Zr zC{qVbcLB(tAbu_*6{Zzsnq_N~=HburmiYL0UyU;D`nlisyId%_?26$Q-?pT9=?O`o ztQRkTH;E3?y*^U1BkSEs)lk&p*l=DQ9+h6Sgx~FZ=TU-SpJkwbjD&St$`~D=>)a9B ze1((F^L?R9G3paVzlhqBg>`pgLifA@YXuTsZ{Rj%Kd&Mwoe&5XS)v7*XA%Jk#pOIp z!~oUR7sWhab)7S0PT{GLn z^-nW+u~d63d`e#E?*nHKxkJmKkf7sQbG&-emn=(vOg{@Syv{FokH~yW#i#Tw-Sx9h zG(=i0&kFJBaYCh-Er`+LNZF+%hd2IH^wmP0_|9_>I-AgBu@=N`I9|2;kO)fQQ$Csa!F&g1)6&Od zV=1KZivW#^Ep|eOk;GN8&YMj>}W! z=gxWs**zy*qUb1RsG`w24REYqA-$58G^nS1v3!sPU$3MdUB$Vt~ry4QEn6s`2rQ(0o(P;04A z=XNQw1#^@fbnL+j=$)urUe{UEnW~PRuKoG80Xc+PbH_^E3B`h`W__OTk#>?WCq}pv zbYBM_`-`k`UkjB=(Iq%4&M$PU_t|Dzt!>&)xwD`s9aLSZ;!NN>DN-)apna|69L^b$ z+tA47VRC=j?hfM;k{KEaZTu{p>_T~H$kofN*G^xOG9UHiAJQeYY*J&N}} z*mvCPBj><7A+lBbSc&Z#=A)EMG2w zj=cNEKVEJfXLB4}ZgO9kpFeE$yg70CV=j#{_qEB4m`oe5YjmA>V(ocBvXf40=@@%` zOcJCoT7Dgtt}3<;Lz10!t&lM6O2?rSGUL`xcUwlbCp@YqLq<5g&UX+0NxZVMWE-#6 zt0Y)0QmfA~ccs=;4c)$01Hy&(cN;7A9)o>cw+sTF85?QoO&~$1s<%eY%@1udsSFO{ zo%HIvD^EknA(Dtqs#?ypE!LJ4vc1;?YK3^2h4|h|?d#_k6G(P++pIDNj7>y)x;$4_ z5j20=s1TFw$`hg2e>rLAk`%g|L9X2#$el_?%O>Gq%KU&^Lic#) zOKJHr{dUo2;wF@GD!&}Oro>H~m3|Qa*#F0TpI-L02cLEG8*#Q?&g{&YrEk;NQ0N){ zO#ZmBL#JT1W~grYaSR>0r!zLbR`4WcFW0e)T9(MJ@B;zE`xAHsrpz;yay5yN&0OPj zB%ax#)$Qvvg8Sy*#YK%lGX2qqQhAHY{Lu9z!>Y}#eyfz6^pNZs`MKNvK-4r#V*EZArxHiQ)5Ht z3~B;a^NUA1^VHPqGNSn^@7WuBEIYF=l*z)I5YITVuKg68BT01jkW>#y+9WJa)>ey4 zOR6&&@Y!$Mo+os|Iq0_>-jatb8J{b*4%fficzx1ly>Yp-CR%_n6j!+PqX}PkQIfxV zv>|u9P!nC*-uXM6(IMK9kYgRTdiZPPjL;J1EqTmak2@*}H{42@)Rc;VK|Xrjsg>Ia z!XIzvzE+{njmNaqPDdKIdcZBthrcE@ty&*dGbZ#jnXNfsp zG0bKvyb&wzN?mtnTEcDQttOb>>}f&x(3`?FI_!tEq`b;st)a!`%o*&)dr0_j<7Rw5jr1gXxjHVpILWg zq>QH<)#z~F5K8v}Jw(VW*X2bS9}lm-R(v`>D-_g8LXbS_Y7?6G&?}9>_IGZ_BFOFT zb7?$Z+evXPPK@^|odT4KFXF0mqdj;>A&ui19j=TX5)QmVaPCZ}$nrMLJ>xG8p-Ab}J@dca z)>3T3iCZpy7hPq9wwOtOe-NMR$|n1{U6VlZeC)bUnt}7H*Dj;FgRg6eh3VM39}kX= z7wWjPm>X?aNRfggJl7~uRro2&t-rU-TVGCO<3>MTnrB!jgjwsyIocMC@=Nv1Zte{5 z(o8*zYUS~-ljuZUO-!e5Um4kPX3ck+Q!&t8#$RQf^?sm`_4KeBEK+Gw2};Y(f?Dnl zj~gGiNWAJCPndqEITQ-nAGM)VnUhGHW`HeGFkd)hv<@axEDziNs4Hga=g)vAn86w;}3%WbvDVVw)MBW&F53})F; zW)R6{&kUqBa*2})>y=7)=hTvR%CcG$gNWSR8~1sLr@1oMWOsLiN5enQHwP2Lc&g5H zJ&08{Z6x$#DuXAbX{~Q)1a!ENK82r6*OV+V1pK6C=sqt;;B`u-bS2NIE-8i3ao7%&dGAo597wkRc8R*8@5Q$)j2V~K#9xXWf zcD?ZwlJDIgHeEsLi`33gA-!bQJA8R+gz_;l+RR;>z0r@3>d13kg(-83Xh9cZf%UJh--QLn;$5)0|!RFcUWX|X952d(w9|l>zqItW+5U#b?WTdR=tGW%SZ+#%~ z=;TjEZQp%aS97FZx7=9oc@sPA_R6UA^_B0NJ=j!?SR#i=>hc45q1Hnsn_Cw#1x#A} zH|e=O>&bX?{5p~)ID)$hxu=BG2MH{8{``#By-b(FzN^{G zX}A-+?DHBe{PyS3c@OYz`;9(}IP{B_g327?tbJ zsAQJR#cEM+LRaoB$1*fU(`mfV!MneKJxGoB1Ut>@y7X*WAFxvdg7gN2*)V7dH}Xa<_w+7Dg20meEk0w$v9h~r15B4%q?qe` z`DpLU9!UJAfaJ>4lyno`LKAp8$QR@UzAwWy;oKgO4bOA)#mb2#P&rsno=A+E$U zVp7q!)v$&;(Mp*+{PxQ&*0(jZVrTPJ9gCfqg4a12Uqs z3$m_YB8)rYG|$#^@lLb^>{8-w{G~@n2aJ3Yvf;Wyv+9myF6tAy}mm9S$(M=vH z>o`3UQTTXpK4P@3`;JsxYtG`X>GoUyJ7Lwm_fYbmE|;^v`sc&%sM}B0opd6LUVc-+ zvWszax%kXNDWi=WR((HUD=Ht7r)!#ZRjqB}kI|}en^WOeig!|beCdCY^EPXs!H2pd zsEMDwl{;(x4js!#IMwt-Uc-OufWBM0nj**&sPQF=hrm# zX>EC6EC+)RI`jFfE-ANl8lr-;`BSTxg+%KCF<`8e0|zkb!KaAL z7!|`?uFWze5w2#s%2)Py+d|m-+E~js{V93aW^0U#XP0QmNjeyiR$Bqt~yrw-h|w)5EE{XCR*Sc4q8FQ|~J~ z|0{+dO~+Mob=Rewe16!TL{eC|-q)fGfzeKSF_N~2_kCxXP98JYHPIUgV>5K+BPS(z zge5rNiPJSTCJAP5eU5c)&ucF@wz)$rwZmo5rP+1#VwhU zy1!?SxAb{*6?8n}HklFUkwM+tv-GLK?f${N{C=qW0axw@RFG~2pBx(~#qoCc1H6Ng zgy~>Oveq>RSwQ87+zO_x8fwma4(Mj>`5*MCUayUcV z!OE+l1JGyd&>U2!`83rF)?48@;*M8UhW+2cy-&tjF%xyd*591I@WRL@FDHqdEmKYm zhMvsw!&pQvH8;UOD9umJEmlxE+2y>#(_~H~7^mmSwVe?nB2MdDWO1U*Ky3RoSu^K+ z9YcnMN}kv=$*wi8!l9($ndg~~d+J7Zk;GAG<;{D+=fdNnTx9Me?uS(EWJ)g5Pm6Gs zy$XL>dQDmG*JEa6a1|+8TDxW9&)*G5T2KJApo-BqmI>v;*$tq>f zQ%ng#-|T@?v@}dft-r8ZF`-tmDBQ|?YaP9`bDpqLwG%9?o#a-GkGU2sNYgR-L(*T$ ztJTR}AoLU?8P0U?S_d6_e@5lxz7{9_{O#0z3Z84rC|#py@P5^$|Hwx>&jHL5?VgFFrVB}Dt%a(lHE#>B~8KIhrGpYb)6@&{KIsCzgicT3@c9m zs;d0W%A0E zi{K(H^tW0}0BVx6hn7_n{Gfq|IxIti{(IU=Q`+MJhX45n<{W-EAcOp7v)0w7mH(GbK z)SzNd89mlfmr46zH>Y$SZJV2Wefi8(7FU6`KCQR<^b7y^j(=325(csoZnc2`r`)HR z{nhDjqt-3f7w3FI1_NnmQ4Q!658DdbF;9~vhditsHS$_6gI!+mKrYk=7}_qTIAgWF z2p+$E9e+Fo*)U!+VA)Bar~)&x(p6AI0h%3sjV=^vi+n^IpO+}|QhIIqC}m`arXT#N zhDF8k##juJrX!L*tXl?jF3x-LSz5xLkB|?ShbDqR69W2)xOk?J?F+-@nz0`0TPxAE z`I+?Euyy!EKPbt4ukgT37`?#?g!I_EER*R6#b-_wL|1v=wR|ua?kg=tU}lOQB8|;p z4+?C^acsU&!nF%lAhvQlnM_mbk{Ft7FRCbQFpj#AE?doK^y!4Xu6m;k&K;F&b7MT( zN=8FG-PN7XoJcYae?H4vj?CUUKcTFA)PZoyh@y))-T3K&%$~Z&Zg>m(DM``5;Z#`6 z&GL@GW@sRB-78-HU8NpX2vWvw8k0WMuj6F%8H&zS4AY2eA`tBS-4Ky1J_3}P!kg$i z-4r$0u2YV^Lz9hLsuDzai0mA*^r9VK5Mf#GwvM%ty|PM&IcN7EU3`NMKsZb2OC@I8 zy{Prz@DP8n=kl7hTLzM=Ew74xj~aT5tJ-SnG=?H&^wYdVuEexjjD8HyvP_rRe6Je; z8=v>ts5uelWAxPhMN6?G_&H-A4yvwc$?y!DWBsVa`;Hg)<#|387IO-l`>Ee-BFo8tIhJk@0z zA*Hg3fqvGJ*)g~?8gn{cAbsi(82j=|DVGYaOF5oLH1&(1w^HA!9J4`4AyF|AVb(_6 zd{|NQ{klhEeD24WoHMumA(k~p&r&aI%xZ8x+~J!}7^WeCLrI(3#Rj8G2cCvYyMvCN zMTdX`WL0odle=*dgFbInp%L?gdDO@TmU-kBEJm>1 z`-^1QANzh{%Po}?z2M&B&n4q%G6)(rqb)cdalsMNRPv<|`f{4&Ql7hUTlsu79t*im*>z8l!fpY>zpBU->^C@ zY#Lz&yX#6A*ZXCn1Fd{E_~0#&BcD`t$PXao!1QZf>tukV8%}cAXoGZw zXV~(Fz{GrI6mR>|{Ecz-!01aMo2L1tdIrP9KhW!O>Nm?gT?~7tnB5wlA(5PS%2yh{% z_uEpcVkeUNL|sajE2(ib)kZbR@lS<>O%c-qM@yTofv0`_Pr4imw^h?3JkuR2D0;m_ zshj6NT@7+QsczQp|Ky}!LEk8Dw|w__H5Sq9lq3wJM|V!Go8_b?IHjyH5k~pCkT2gH z>GmrYc>!e?c`AbyOHg&T^sJr$!KSqW%DFx?`}-QP@mGBxq(exch>-6q$|d^>)+x)4 zK&LM7&0(=;Yai-;MmD}b--nOB_MkFppp@aqj#S_~qw5@*Y0j|9v<1;&RHDx^ey4Wr zT)FiR*S)S`ua<(Nq2#xQ?dnVB>#mh@9+p$6fUa>sz58TqhAZV+MP{E!wP0)}M&Qqv zNWCrgg^kLqh{H8qmC2}$M|7R0WC4cm#z;Ed^6U=2`gLFEwWp9`C2|YG*Yc7c(08l0 zK<{_^am&`>eB>8qH9@cWge%N_Xs1Xn_w=e0Q|Hl@y&O;Ht_``s+?vuFC;1%BW3fu@ zg&_|+>gpIS+KH(eam=voLxoy8v}cr@Wt_BbY^Kmq21ByJh0k$YYraHUMcc43NE}vv zq#t|sY!&A{0tW9G+Qxi_F0Cf4$;11oJY(SvhBp$3RwL&suUM;v)mJ`(Anef@si1_V z&IafC#eAsMK8ckJO^kmdvSH@GV*>0C>W>{;AAII*+(8m*A0&mM)*d|N2F{z%DV-uf{ zQM$R?>qZi9(dz{t@7%AbKef6Eyu{aG+0pOg8JM>#M|JjV@Ls=~jfXMpL3|FehO#W> zuX}0+-_*^W!glFmIQ!E1+jr)+DC6#_CpXPFcuC8R~fBtHMX7aE% ziYBK(E|B7ahClCS0g1)(wv|~C{A@H@lI7;o$41U%E?xbxPf}85@tvp-4MG&3BZo9* zt)ZyJwrCN4({+&FP0c-maY*OLK*A@D_oO#Dh6h9XRFu>LJ><#UQP9HeH#sEhH_v$~ zPoPCpktD|aMYFbeNg#i0keCU*Qw_&rIXXof8w01;!+<9g{YZzv2_rA~lyR5@(_wt3 z>bA{m5HYnOjAiC z9d&!Xu$gf8pbYs<=DP$VsNR>(SO%nxOL(n4Rj&Nsa$UYx9&{BpG-_zsT!uu+!=3HB zrxmiWkn4|OZv=AUnBosAHOyh|)Lt+}V*Uv&<;x8-S$qNd+KS? z(0E)24t?2ObRfn)yirui%k4F3=+o3tgK3>9r)oPq2qe8<_-NZ{CcL%T2}&I<&1%(;#dGa)WQ^DO1}IuXB_ve$U>TK3AiUe3b>yOv`kHD!d$iY`7yWNRy&D5lF< z8Q1g;($woTv?JeKdg2G1l)Yl(Y?u;EV#&;DQg$&+-j|R{tbr@#HmeFJxI=C09+7*Q zE?mT{(qB~i9UX$6h{hV(`ipxm9uKO>g_*~d;I8aP2iqMs$}@&oYiM|oyM;`|CCRIkSSOKY%HO%BoKI2m@{&j9 zO8+~wbS0Ldz4 z$tB-OA`Op<5r=mIe0OsW3P=@}#^nyM4I!r+nzsqAYw%AqHh<5vaz7!fZ{L?#ie?pH zk4Jtr#I*xz1`8g>D1M)y*s5Wn&bA8RHeGIf$l!IoT+UA*Q4?bydKOJ`IvYmWD%oD% zqf9aVg_}y0uWxVWq}8|agtkUBlxngmM^!&vI;Io5f0LSbYK^m*ZzUmY?ql4H@e|!a zsgl%+&gZ?&(nBOUp-m;{3bgu`nNpO~)g#`0rjwW=vf^L$1X}hA!6jViri$x^oA0%1 zde6!#%$-?j6?3V*)(vwrU2?0Nh=Sa`t`lE{txE3Nh`*~enmd_uml;X*y;0vrE8sG3(M5{MdT+Ne! z-9K82IcOCnE<$J7ekj5i-X00WzD$8bO?J&@Oo&!2j*64aM}r+J;?ke`%2`f{C#M*D z%FGtUw_JqmCc=*Kd`?tmK7uEV4Ke zR%mY>n5elRlhRo=v+Nn9i;Yc*7sQpFV_ay&kK~QmYg9qhJv9dUFQU6tVe1aN`W`IT zu$-KWtmP35Ou`OQ3u(C&W~cj7-8BjGde#M@bw&aMgP)Lb4X>id)ZK)3 zgr{yu`=3SFXcomejE026FU=|tt~9}(^cn}hjmn{4BD)av-O?eo*KZ^mO;8V{+&qBE z3NiB+=7NTwyJq3`%D6M}{$7)KE*KqK13_)=j%bV%$>97E(Sk4&T%>I5I->54PT9D7 z>G%-YesmlwIc;unmVYaSR23xOg0s^?8v z!Oi^UpXA!Nc=N{O6zcO=a6`FB=r3_WThu(0+_1i#hqe~?a8R&nd0xqdGCMDNJGGs! z5fktam>OuFti|hTJTFAG%BBi|sx%KSmDTHvLA$90Hyof3G8bQbR%)or(aRwEu<&fr zeKq}1!PmCsv<1l$<%MuzP=;H2^%h!L{06=?E!-PbbfqJtl$E;ec|)dTzvf(P*~J2` zJ@Nfggl#o!0tQo-S*c9apg^LR)}9|RkLIUb7dQ745LhLCXkQas1X&XP{Qdv`2j~JG AB>(^b literal 0 HcmV?d00001 diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/MergeOfflineRegionsActivity.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/MergeOfflineRegionsActivity.kt new file mode 100644 index 000000000..9905733a6 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/MergeOfflineRegionsActivity.kt @@ -0,0 +1,129 @@ +package com.mapbox.mapboxsdk.testapp.activity.offline + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.support.v4.app.ActivityCompat +import android.support.v4.content.ContextCompat +import android.support.v7.app.AppCompatActivity +import android.widget.Toast +import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.constants.Style +import com.mapbox.mapboxsdk.log.Logger +import com.mapbox.mapboxsdk.offline.OfflineManager +import com.mapbox.mapboxsdk.offline.OfflineRegion +import com.mapbox.mapboxsdk.testapp.R +import kotlinx.android.synthetic.main.activity_merge_offline_regions.* +import java.io.File +import java.io.FileOutputStream + +class MergeOfflineRegionsActivity : AppCompatActivity() { + companion object { + private const val TEST_DB_FILE_NAME = "offline.db" + private const val PERMISSIONS_REQUEST_CODE = 11 + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_merge_offline_regions) + + // forcing offline state + Mapbox.setConnected(false) + + mapView.setStyleUrl(Style.SATELLITE) + + mapView.onCreate(savedInstanceState) + load_region_btn.setOnClickListener { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_CODE) + } else { + mergeDb() + } + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PERMISSIONS_REQUEST_CODE) { + for (result in grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + finish() + } + } + mergeDb() + } + } + + private fun mergeDb() { + // copy db asset to internal memory + copyAsset() + + OfflineManager.getInstance(this).mergeOfflineRegions( + this.filesDir.absolutePath + "/" + TEST_DB_FILE_NAME, + object : OfflineManager.MergeOfflineRegionsCallback { + override fun onMerge(offlineRegions: Array) { + mapView.setStyleUrl(Style.SATELLITE) + Toast.makeText( + this@MergeOfflineRegionsActivity, + String.format("Merged %d regions.", offlineRegions.size), + Toast.LENGTH_LONG).show() + } + + override fun onError(error: String) { + Logger.e("MBGL_OFFLINE_DB_MERGE", error) + } + }) + } + + private fun copyAsset() { + val bufferSize = 1024 + val assetManager = this.assets + val inputStream = assetManager.open(TEST_DB_FILE_NAME) + val outputStream = FileOutputStream(File(this.filesDir, TEST_DB_FILE_NAME)) + + try { + inputStream.copyTo(outputStream, bufferSize) + } finally { + inputStream.close() + outputStream.flush() + outputStream.close() + } + } + + override fun onStart() { + super.onStart() + mapView.onStart() + } + + override fun onResume() { + super.onResume() + mapView.onResume() + } + + override fun onPause() { + super.onPause() + mapView.onPause() + } + + override fun onStop() { + super.onStop() + mapView.onStop() + } + + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } +} diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_simple.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_simple.xml index 96a3f5b04..e67740ad5 100644 --- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_simple.xml +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_map_simple.xml @@ -1,6 +1,5 @@ - + android:layout_height="match_parent" /> - + diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_merge_offline_regions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_merge_offline_regions.xml new file mode 100644 index 000000000..5c610418a --- /dev/null +++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_merge_offline_regions.xml @@ -0,0 +1,20 @@ + + + + + +