diff --git a/app/src/main/java/mobi/maptrek/MapTrek.java b/app/src/main/java/mobi/maptrek/MapTrek.java index f6f03d1e..bec0c4c1 100644 --- a/app/src/main/java/mobi/maptrek/MapTrek.java +++ b/app/src/main/java/mobi/maptrek/MapTrek.java @@ -314,8 +314,7 @@ public Index getMapIndex() { public MapIndex getExtraMapIndex() { if (mExtraMapIndex == null) { mExtraMapIndex = new MapIndex(this, getExternalFilesDir("maps")); - mExtraMapIndex.initializeOfflineMapProviders(); - mExtraMapIndex.initializeOnlineMapProviders(); + mExtraMapIndex.initializePluginMapProviders(); } return mExtraMapIndex; } diff --git a/app/src/main/java/mobi/maptrek/maps/MapIndex.java b/app/src/main/java/mobi/maptrek/maps/MapIndex.java index c8ef3eb0..856edcc3 100644 --- a/app/src/main/java/mobi/maptrek/maps/MapIndex.java +++ b/app/src/main/java/mobi/maptrek/maps/MapIndex.java @@ -18,9 +18,7 @@ import android.annotation.SuppressLint; import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -43,10 +41,9 @@ import java.util.List; import java.util.Objects; -import mobi.maptrek.maps.offline.OfflineTileSource; -import mobi.maptrek.maps.offline.OfflineTileSourceFactory; -import mobi.maptrek.maps.online.OnlineTileSource; -import mobi.maptrek.maps.online.OnlineTileSourceFactory; +import mobi.maptrek.maps.plugin.PluginOfflineTileSource; +import mobi.maptrek.maps.plugin.PluginOnlineTileSource; +import mobi.maptrek.maps.plugin.PluginTileSourceFactory; import mobi.maptrek.util.FileList; import mobi.maptrek.util.MapFilenameFilter; @@ -58,10 +55,12 @@ public class MapIndex implements Serializable { private final Context mContext; private final HashSet mMaps; + private final PluginTileSourceFactory mPluginTileSourceFactory; @SuppressLint("UseSparseArrays") public MapIndex(@NonNull Context context, @Nullable File root) { mContext = context; + mPluginTileSourceFactory = new PluginTileSourceFactory(context, context.getPackageManager()); mMaps = new HashSet<>(); if (root != null) { logger.debug("MapIndex({})", root.getAbsolutePath()); @@ -118,58 +117,26 @@ private void loadMap(@NonNull File file) { mMaps.add(mapFile); } - public void initializeOnlineMapProviders() { - PackageManager packageManager = mContext.getPackageManager(); - - Intent initializationIntent = new Intent("mobi.maptrek.maps.online.provider.action.INITIALIZE"); - // enumerate online map providers - List providers = packageManager.queryBroadcastReceivers(initializationIntent, 0); - for (ResolveInfo provider : providers) { - // send initialization broadcast, we send it directly instead of sending - // one broadcast for all plugins to wake up stopped plugins: - // http://developer.android.com/about/versions/android-3.1.html#launchcontrols - Intent intent = new Intent(); - intent.setClassName(provider.activityInfo.packageName, provider.activityInfo.name); - intent.setAction(initializationIntent.getAction()); - mContext.sendBroadcast(intent); - - List tileSources = OnlineTileSourceFactory.fromPlugin(mContext, packageManager, provider); - for (OnlineTileSource tileSource : tileSources) { - MapFile mapFile = new MapFile(tileSource.getName(), tileSource.getUri()); - mapFile.tileSource = tileSource; - mapFile.boundingBox = WORLD_BOUNDING_BOX; - //TODO Implement tile cache expiration - //tileProvider.tileExpiration = onlineMapTileExpiration; - mMaps.add(mapFile); - } + public void initializePluginMapProviders() { + for (PluginOnlineTileSource source : mPluginTileSourceFactory.getOnlineTileSources()) { + addTileSource(source, source.getSourceId()); } - } - public void initializeOfflineMapProviders() { - PackageManager packageManager = mContext.getPackageManager(); - - Intent initializationIntent = new Intent("mobi.maptrek.maps.offline.provider.action.INITIALIZE"); - // enumerate offline map providers - List providers = packageManager.queryBroadcastReceivers(initializationIntent, 0); - for (ResolveInfo provider : providers) { - // send initialization broadcast, we send it directly instead of sending - // one broadcast for all plugins to wake up stopped plugins: - // http://developer.android.com/about/versions/android-3.1.html#launchcontrols - Intent intent = new Intent(); - intent.setClassName(provider.activityInfo.packageName, provider.activityInfo.name); - intent.setAction(initializationIntent.getAction()); - mContext.sendBroadcast(intent); - - List tileSources = OfflineTileSourceFactory.fromPlugin(mContext, packageManager, provider); - for (OfflineTileSource tileSource : tileSources) { - MapFile mapFile = new MapFile(tileSource.getName(), tileSource.getUri()); - mapFile.tileSource = tileSource; - mapFile.boundingBox = WORLD_BOUNDING_BOX; - mMaps.add(mapFile); - } + for (PluginOfflineTileSource source : mPluginTileSourceFactory.getOfflineTileSources()) { + addTileSource(source, source.getSourceId()); } } + private void addTileSource(TileSource tileSource, String id) { + MapFile mapFile = new MapFile(tileSource.getName(), id); + mapFile.tileSource = tileSource; + mapFile.boundingBox = WORLD_BOUNDING_BOX; + //TODO Implement tile cache expiration + //tileProvider.tileExpiration = onlineMapTileExpiration; + + mMaps.add(mapFile); + } + @NonNull public ArrayList getMaps(@Nullable String[] ids) { ArrayList maps = new ArrayList<>(); diff --git a/app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSourceFactory.java b/app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSourceFactory.java deleted file mode 100644 index d7d5b540..00000000 --- a/app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSourceFactory.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2020 Andrey Novikov - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - * - */ - -package mobi.maptrek.maps.offline; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.List; - -public class OfflineTileSourceFactory { - @NonNull - public static List fromPlugin(Context context, PackageManager packageManager, ResolveInfo provider) { - List sources = new ArrayList<>(); - - int id; - String[] maps = null; - try { - Resources resources = packageManager.getResourcesForApplication(provider.activityInfo.applicationInfo); - id = resources.getIdentifier("maps", "array", provider.activityInfo.packageName); - if (id != 0) - maps = resources.getStringArray(id); - - if (maps == null) - return sources; - - for (String map : maps) { - String name = null; - String uri = null; - id = resources.getIdentifier(map + "_name", "string", provider.activityInfo.packageName); - if (id != 0) - name = resources.getString(id); - id = resources.getIdentifier(map + "_uri", "string", provider.activityInfo.packageName); - if (id != 0) - uri = resources.getString(id); - if (name == null || uri == null) - continue; - OfflineTileSource.Builder builder = OfflineTileSource.builder(context); - builder.name(name); - builder.code(map); - builder.uri(uri); - - id = resources.getIdentifier(map + "_license", "string", provider.activityInfo.packageName); - if (id != 0) - builder.license(resources.getString(id)); - id = resources.getIdentifier(map + "_minzoom", "integer", provider.activityInfo.packageName); - if (id != 0) - builder.zoomMin(resources.getInteger(id)); - id = resources.getIdentifier(map + "_maxzoom", "integer", provider.activityInfo.packageName); - if (id != 0) - builder.zoomMax(resources.getInteger(id)); - - OfflineTileSource source = builder.build(); - sources.add(source); - } - } catch (Resources.NotFoundException | PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return sources; - } -} diff --git a/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSource.java b/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSource.java deleted file mode 100644 index 6b3fe3f9..00000000 --- a/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSource.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2018 Andrey Novikov - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - * - */ - -package mobi.maptrek.maps.online; - -import android.content.ContentProviderClient; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; - -import org.oscim.core.Tile; -import org.oscim.tiling.source.OkHttpEngine; -import org.oscim.tiling.source.bitmap.BitmapTileSource; - -public class OnlineTileSource extends BitmapTileSource { - public static final String TILE_TYPE = "vnd.android.cursor.item/vnd.mobi.maptrek.maps.online.provider.tile"; - public static final String[] TILE_COLUMNS = new String[]{"TILE"}; - - public static class Builder> extends BitmapTileSource.Builder { - private Context context; - protected String name; - protected String code; - protected String uri; - protected String license; - protected int threads; - - protected Builder(Context context) { - this.context = context; - // Fake url to skip UrlTileSource exception - this.url = "http://maptrek.mobi/"; - //FIXME Switch to Volley http://developer.android.com/training/volley/index.html - this.httpFactory(new OkHttpEngine.OkHttpFactory()); - } - - @Override - public OnlineTileSource build() { - return new OnlineTileSource(this); - } - - public T name(String name) { - this.name = name; - return self(); - } - - public T code(String code) { - this.code = code; - return self(); - } - - public T uri(String uri) { - this.uri = uri; - return self(); - } - - public T license(String license) { - this.license = license; - return self(); - } - - public T threads(int threads) { - this.threads = threads; - return self(); - } - } - - @SuppressWarnings("rawtypes") - public static Builder builder(Context context) { - return new Builder(context); - } - - private final Context mContext; - private ContentProviderClient mProviderClient; - - private final String mName; - private final String mCode; - private final String mUri; - private final String mLicense; - private final int mThreads; - - protected OnlineTileSource(Builder builder) { - super(builder); - mContext = builder.context; - mName = builder.name; - mCode = builder.code; - mUri = builder.uri; - mLicense = builder.license; - mThreads = builder.threads; - } - - @Override - public OpenResult open() { - mProviderClient = mContext.getContentResolver().acquireContentProviderClient(Uri.parse(mUri)); - if (mProviderClient != null) - return OpenResult.SUCCESS; - else - return new OpenResult("Failed to get provider for uri: " + mUri); - } - - @Override - public void close() { - if (mProviderClient != null) { - mProviderClient.release(); - mProviderClient = null; - } - } - - @Override - public String getTileUrl(Tile tile) { - String tileUrl = null; - if (mProviderClient == null) - return null; - Uri contentUri = Uri.parse(mUri + "/" + tile.zoomLevel + "/" + tile.tileX + "/" + tile.tileY); - try { - Cursor cursor = mProviderClient.query(contentUri, TILE_COLUMNS, null, null, null); - if (cursor != null) { - cursor.moveToFirst(); - tileUrl = cursor.getString(0); - cursor.close(); - } - } catch (RemoteException e) { - e.printStackTrace(); - } - return tileUrl; - } - - public String getName() { - return mName; - } - - public String getUri() { - return mUri; - } -} diff --git a/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSourceFactory.java b/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSourceFactory.java deleted file mode 100644 index 6e50356d..00000000 --- a/app/src/main/java/mobi/maptrek/maps/online/OnlineTileSourceFactory.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2018 Andrey Novikov - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - * - */ - -package mobi.maptrek.maps.online; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import androidx.annotation.NonNull; - -import org.oscim.android.cache.TileCache; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -public class OnlineTileSourceFactory { - @NonNull - public static List fromPlugin(Context context, PackageManager packageManager, ResolveInfo provider) { - List sources = new ArrayList<>(); - - int id; - String[] maps = null; - try { - Resources resources = packageManager.getResourcesForApplication(provider.activityInfo.applicationInfo); - id = resources.getIdentifier("maps", "array", provider.activityInfo.packageName); - if (id != 0) - maps = resources.getStringArray(id); - - if (maps == null) - return sources; - - File cacheDir = new File(context.getExternalCacheDir(), "online"); - boolean useCache = cacheDir.mkdir() || cacheDir.isDirectory(); - - for (String map : maps) { - String name = null; - String uri = null; - id = resources.getIdentifier(map + "_name", "string", provider.activityInfo.packageName); - if (id != 0) - name = resources.getString(id); - id = resources.getIdentifier(map + "_uri", "string", provider.activityInfo.packageName); - if (id != 0) - uri = resources.getString(id); - if (name == null || uri == null) - continue; - OnlineTileSource.Builder builder = OnlineTileSource.builder(context); - builder.name(name); - builder.code(map); - builder.uri(uri); - - id = resources.getIdentifier(map + "_license", "string", provider.activityInfo.packageName); - if (id != 0) - builder.license(resources.getString(id)); - id = resources.getIdentifier(map + "_threads", "integer", provider.activityInfo.packageName); - if (id != 0) - builder.threads(resources.getInteger(id)); - id = resources.getIdentifier(map + "_minzoom", "integer", provider.activityInfo.packageName); - if (id != 0) - builder.zoomMin(resources.getInteger(id)); - id = resources.getIdentifier(map + "_maxzoom", "integer", provider.activityInfo.packageName); - if (id != 0) - builder.zoomMax(resources.getInteger(id)); - - OnlineTileSource source = builder.build(); - if (useCache) { - TileCache cache = new TileCache(context, cacheDir.getAbsolutePath(), map); - source.setCache(cache); - } - sources.add(source); - } - } catch (Resources.NotFoundException | PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return sources; - } -} diff --git a/app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSource.java b/app/src/main/java/mobi/maptrek/maps/plugin/PluginOfflineTileSource.java similarity index 52% rename from app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSource.java rename to app/src/main/java/mobi/maptrek/maps/plugin/PluginOfflineTileSource.java index 84d1a779..0bb27815 100644 --- a/app/src/main/java/mobi/maptrek/maps/offline/OfflineTileSource.java +++ b/app/src/main/java/mobi/maptrek/maps/plugin/PluginOfflineTileSource.java @@ -1,26 +1,12 @@ -/* - * Copyright 2020 Andrey Novikov - * - * This program is free software: you can redistribute it and/or modify it under the - * terms of the GNU Lesser General Public License as published by the Free Software - * Foundation, either version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with - * this program. If not, see . - * - */ - -package mobi.maptrek.maps.offline; +package mobi.maptrek.maps.plugin; + +import static org.oscim.tiling.QueryResult.FAILED; +import static org.oscim.tiling.QueryResult.SUCCESS; +import static org.oscim.tiling.QueryResult.TILE_NOT_FOUND; import android.content.ContentProviderClient; import android.content.Context; -import android.database.Cursor; import android.graphics.BitmapFactory; -import android.net.Uri; import android.os.RemoteException; import org.oscim.android.canvas.AndroidBitmap; @@ -30,47 +16,36 @@ import org.oscim.tiling.ITileDataSource; import org.oscim.tiling.TileSource; -import static org.oscim.tiling.QueryResult.FAILED; -import static org.oscim.tiling.QueryResult.SUCCESS; -import static org.oscim.tiling.QueryResult.TILE_NOT_FOUND; - -public class OfflineTileSource extends TileSource { - public static final String TILE_TYPE = "vnd.android.cursor.item/vnd.mobi.maptrek.maps.offline.provider.tile"; - public static final String[] TILE_COLUMNS = new String[]{"TILE"}; +public class PluginOfflineTileSource extends TileSource { public static class Builder> extends TileSource.Builder { - private Context context; - protected String name; - protected String code; - protected String uri; - protected String license; + private final Context context; + protected String mapName; + protected String mapId; + protected String providerAuthority; protected Builder(Context context) { this.context = context; } @Override - public OfflineTileSource build() { - return new OfflineTileSource(this); + public PluginOfflineTileSource build() { + return new PluginOfflineTileSource(this); } + @Override public T name(String name) { - this.name = name; - return self(); - } - - public T code(String code) { - this.code = code; + this.mapName = name; return self(); } - public T uri(String uri) { - this.uri = uri; + public T mapId(String identifier) { + this.mapId = identifier; return self(); } - public T license(String license) { - this.license = license; + public T providerAuthority(String authority) { + this.providerAuthority = authority; return self(); } } @@ -83,27 +58,27 @@ public static Builder builder(Context context) { private final Context mContext; private ContentProviderClient mProviderClient; - private final String mName; - private final String mCode; - private final String mUri; - private final String mLicense; + private final String mMapName; + private final String mMapId; + private final String mProviderAuthority; + private final String mSourceId; - protected OfflineTileSource(Builder builder) { + protected PluginOfflineTileSource(Builder builder) { super(builder); mContext = builder.context; - mName = builder.name; - mCode = builder.code; - mUri = builder.uri; - mLicense = builder.license; + mMapName = builder.mapName; + mMapId = builder.mapId; + mProviderAuthority = builder.providerAuthority; + mSourceId = "content://" + builder.providerAuthority + "/" + builder.mapId; } @Override public OpenResult open() { - mProviderClient = mContext.getContentResolver().acquireContentProviderClient(Uri.parse(mUri)); + mProviderClient = mContext.getContentResolver().acquireContentProviderClient(mProviderAuthority); if (mProviderClient != null) return OpenResult.SUCCESS; else - return new OpenResult("Failed to get provider for uri: " + mUri); + return new OpenResult("Failed to get provider for authority: " + mProviderAuthority); } @Override @@ -131,14 +106,13 @@ public void query(MapTile tile, ITileDataSink mapDataSink) { mapDataSink.completed(FAILED); return; } - Uri contentUri = Uri.parse(mUri + "/" + tile.zoomLevel + "/" + tile.tileX + "/" + tile.tileY); + + byte[] blob; try { - Cursor cursor = mProviderClient.query(contentUri, TILE_COLUMNS, null, null, null); - if (cursor != null) { - cursor.moveToFirst(); - byte[] bytes = cursor.getBlob(0); - cursor.close(); - Bitmap bitmap = new AndroidBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length)); + blob = PluginTileSourceContract.Tiles.getTileBlob( + mProviderClient, mProviderAuthority, mMapId, tile.zoomLevel, tile.tileX, tile.tileY); + if (blob != null) { + Bitmap bitmap = new AndroidBitmap(BitmapFactory.decodeByteArray(blob, 0, blob.length)); if (bitmap.isValid()) { mapDataSink.setTileImage(bitmap); mapDataSink.completed(SUCCESS); @@ -148,7 +122,7 @@ public void query(MapTile tile, ITileDataSink mapDataSink) { } else { mapDataSink.completed(TILE_NOT_FOUND); } - } catch (RemoteException e) { + } catch (RemoteException | PluginTileSourceContractViolatedException e) { e.printStackTrace(); mapDataSink.completed(FAILED); } @@ -163,11 +137,16 @@ public void cancel() { } } + @Override public String getName() { - return mName; + return mMapName; + } + + public String getMapId() { + return mMapId; } - public String getUri() { - return mUri; + public String getSourceId() { + return mSourceId; } } diff --git a/app/src/main/java/mobi/maptrek/maps/plugin/PluginOnlineTileSource.java b/app/src/main/java/mobi/maptrek/maps/plugin/PluginOnlineTileSource.java new file mode 100644 index 00000000..29816ed7 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/maps/plugin/PluginOnlineTileSource.java @@ -0,0 +1,116 @@ +package mobi.maptrek.maps.plugin; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.os.RemoteException; + +import org.oscim.core.Tile; +import org.oscim.tiling.source.OkHttpEngine; +import org.oscim.tiling.source.bitmap.BitmapTileSource; + +public class PluginOnlineTileSource extends BitmapTileSource { + + public static class Builder> extends BitmapTileSource.Builder { + private final Context context; + protected String mapName; + protected String mapId; + protected String providerAuthority; + + protected Builder(Context context) { + this.context = context; + // Fake url to skip UrlTileSource exception + this.url = "http://maptrek.mobi/"; + //FIXME Switch to Volley http://developer.android.com/training/volley/index.html + this.httpFactory(new OkHttpEngine.OkHttpFactory()); + } + + @Override + public PluginOnlineTileSource build() { + return new PluginOnlineTileSource(this); + } + + @Override + public T name(String name) { + this.mapName = name; + return self(); + } + + public T mapId(String identifier) { + this.mapId = identifier; + return self(); + } + + public T providerAuthority(String authority) { + this.providerAuthority = authority; + return self(); + } + } + + @SuppressWarnings("rawtypes") + public static Builder builder(Context context) { + return new Builder(context); + } + + private final Context mContext; + private ContentProviderClient mProviderClient; + + private final String mMapName; + private final String mMapId; + private final String mProviderAuthority; + private final String mSourceId; + + protected PluginOnlineTileSource(Builder builder) { + super(builder); + mContext = builder.context; + mMapName = builder.mapName; + mMapId = builder.mapId; + mProviderAuthority = builder.providerAuthority; + mSourceId = "content://" + builder.providerAuthority + "/" + builder.mapId; + } + + @Override + public OpenResult open() { + mProviderClient = mContext.getContentResolver().acquireContentProviderClient(mProviderAuthority); + if (mProviderClient != null) + return OpenResult.SUCCESS; + else + return new OpenResult("Failed to get provider for authority: " + mProviderAuthority); + } + + @Override + public void close() { + if (mProviderClient != null) { + mProviderClient.release(); + mProviderClient = null; + } + } + + @Override + public String getTileUrl(Tile tile) { + String tileUrl = null; + if (mProviderClient == null) + return null; + + try { + tileUrl = PluginTileSourceContract.Tiles.getTileUri( + mProviderClient, mProviderAuthority, mMapId, tile.zoomLevel, tile.tileX, tile.tileY); + } catch (RemoteException | PluginTileSourceContractViolatedException e) { + e.printStackTrace(); + } + + return tileUrl; + } + + @Override + public String getName() { + return mMapName; + } + + public String getMapId() { + return mMapId; + } + + public String getSourceId() { + return mSourceId; + } +} diff --git a/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContract.java b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContract.java new file mode 100644 index 00000000..a842116d --- /dev/null +++ b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContract.java @@ -0,0 +1,193 @@ +package mobi.maptrek.maps.plugin; + +import android.content.ContentProviderClient; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedList; +import java.util.List; + +import mobi.maptrek.MainActivity; + +/** + * Plugins must declare a receiver accepting ONLINE_PROVIDER_INTENT or OFFLINE_PROVIDER_INTENT. + * They must provide the authority of the ContentProvider serving maps in a string resource + * named "authority". + * + * This ContentProvider must accept two types of queries: + * - "content://authority/maps" must be answered with a list of map descriptions. + * Each map is described by two columns, its name and its identifier. + * - "content://authority/tiles/mapIdentifier/zoomLevel/tileX/tileY" must be answered with the + * the target tile uri or blob. + */ +final class PluginTileSourceContract { + + final static String ONLINE_PROVIDER_INTENT = "mobi.maptrek.maps.online.provider.action.INITIALIZE"; + + final static String OFFLINE_PROVIDER_INTENT = "mobi.maptrek.maps.offline.provider.action.INITIALIZE"; + + final static String[] MAP_COLUMNS = new String[]{"NAME", "IDENTIFIER"}; + + final static String[] TILE_COLUMNS = new String[]{"TILE"}; + + @NonNull + static String getProviderAuthority(@NonNull PackageManager packageManager, @NonNull ResolveInfo provider) + throws PluginTileSourceContractViolatedException, PackageManager.NameNotFoundException { + + String authority; + Resources resources = packageManager.getResourcesForApplication(provider.activityInfo.applicationInfo); + int id = resources.getIdentifier("authority", "string", provider.activityInfo.packageName); + + if (id == 0) { + throw new PluginTileSourceContractViolatedException("Cannot find ContentProvider's authority."); + } else try { + authority = resources.getString(id); + } catch (Resources.NotFoundException e) { + throw new IllegalStateException("Identifier no longer valid.", e); + } + return authority; + } + + static class Maps { + + static final class Metadata { + private final String mName; + private final String mIdentifier; + + Metadata(@NonNull String name, @NonNull String identifier) { + mName = name; + mIdentifier = identifier; + } + + @NonNull String getName() { + return mName; + } + + @NonNull String getIdentifier() { + return mIdentifier; + } + } + + @NonNull + static List getMaps(@NonNull ContentProviderClient client, @NonNull String authority) + throws RemoteException, PluginTileSourceContractViolatedException { + + Uri queryUri = getMapsQueryUri(authority); + List maps = new LinkedList<>(); + Cursor cursor = client.query(queryUri, PluginTileSourceContract.MAP_COLUMNS, null, null, null); + if (cursor == null) + return maps; + + cursor.moveToFirst(); + do { + String name = getMapName(cursor); + String identifier = getMapIdentifier(cursor); + Metadata map = new Metadata(name, identifier); + maps.add(map); + } while (cursor.moveToNext()); + + cursor.close(); + return maps; + } + + @NonNull + private static Uri getMapsQueryUri(@NonNull String authority) { + String uri = "content://" + authority + "/maps"; + return Uri.parse(uri); + } + + @NonNull + private static String getMapName(Cursor cursor) throws PluginTileSourceContractViolatedException { + if (cursor.getColumnCount() < 1) { + throw new PluginTileSourceContractViolatedException("Not enough columns in cursor."); + } + + if (cursor.getType(0) != Cursor.FIELD_TYPE_STRING) { + throw new PluginTileSourceContractViolatedException("Map name must be a string."); + } + + return cursor.getString(0); + } + + @NonNull + private static String getMapIdentifier(Cursor cursor) throws PluginTileSourceContractViolatedException { + if (cursor.getColumnCount() < 2) { + throw new PluginTileSourceContractViolatedException("Not enough columns in cursor."); + } + + if (cursor.getType(1) != Cursor.FIELD_TYPE_STRING) { + throw new PluginTileSourceContractViolatedException("Map identifier must be a string."); + } + + return cursor.getString(1); + } + } + + static class Tiles { + + @Nullable + static String getTileUri(ContentProviderClient client, String authority, String map, int zoomLevel, int tileX, int tileY) + throws RemoteException, PluginTileSourceContractViolatedException { + + Uri queryUri = getTileQueryUri(authority, map, zoomLevel, tileX, tileY); + Cursor cursor = client.query(queryUri, TILE_COLUMNS, null, null, null); + if (cursor == null) { + return null; + } + + cursor.moveToFirst(); + + if (cursor.getColumnCount() < 1) { + throw new PluginTileSourceContractViolatedException("Not enough column in cursor."); + } + + if (cursor.getType(0) != Cursor.FIELD_TYPE_STRING) { + throw new PluginTileSourceContractViolatedException("Unexpected value type."); + } + + String tileUri = cursor.getString(0); + cursor.close(); + return tileUri; + } + + @Nullable + static byte[] getTileBlob(ContentProviderClient client, String authority, String map, int zoomLevel, int tileX, int tileY) + throws RemoteException, PluginTileSourceContractViolatedException { + + Uri queryUri = getTileQueryUri(authority, map, zoomLevel, tileX, tileY); + Cursor cursor = client.query(queryUri, TILE_COLUMNS, null, null, null); + if (cursor == null) { + return null; + } + + cursor.moveToFirst(); + + if (cursor.getColumnCount() < 1) { + throw new PluginTileSourceContractViolatedException("Not enough column in cursor."); + } + + if (cursor.getType(0) != Cursor.FIELD_TYPE_BLOB) { + throw new PluginTileSourceContractViolatedException("Unexpected value type."); + } + + byte[] tileBlob = cursor.getBlob(0); + cursor.close(); + return tileBlob; + } + + private static Uri getTileQueryUri(String authority, String map, int zoomLevel, int tileX, int tileY) { + String uri = "content://" + authority + "/tiles/" + map + "/" + zoomLevel + "/" + tileX + "/" + tileY; + return Uri.parse(uri); + } + } +} diff --git a/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContractViolatedException.java b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContractViolatedException.java new file mode 100644 index 00000000..ff93a925 --- /dev/null +++ b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceContractViolatedException.java @@ -0,0 +1,8 @@ +package mobi.maptrek.maps.plugin; + +public class PluginTileSourceContractViolatedException extends Exception { + + PluginTileSourceContractViolatedException(String msg) { + super(msg); + } +} diff --git a/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceFactory.java b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceFactory.java new file mode 100644 index 00000000..1078739c --- /dev/null +++ b/app/src/main/java/mobi/maptrek/maps/plugin/PluginTileSourceFactory.java @@ -0,0 +1,147 @@ +package mobi.maptrek.maps.plugin; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.RemoteException; + +import androidx.annotation.NonNull; + +import org.oscim.android.cache.TileCache; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +public final class PluginTileSourceFactory { + + private final Context mContext; + private final PackageManager mPackageManager; + + public PluginTileSourceFactory(Context context, PackageManager packageManager) { + mContext = context; + mPackageManager = packageManager; + } + + public List getOnlineTileSources() { + Intent initializationIntent = new Intent(PluginTileSourceContract.ONLINE_PROVIDER_INTENT); + List providers = getMapProviders(initializationIntent); + + List tileSources = new LinkedList<>(); + for (ResolveInfo provider: providers) { + List onlineTileSources = getOnlineMapsFromPlugin(provider); + tileSources.addAll(onlineTileSources); + } + return tileSources; + } + + public List getOfflineTileSources() { + Intent initializationIntent = new Intent(PluginTileSourceContract.OFFLINE_PROVIDER_INTENT); + List providers = getMapProviders(initializationIntent); + + List tileSources = new LinkedList<>(); + for (ResolveInfo provider: providers) { + List offlineTileSources = getOfflineMapsFromPlugin(provider); + tileSources.addAll(offlineTileSources); + } + return tileSources; + } + + private List getMapProviders(Intent initializationIntent) { + List providers = mPackageManager.queryBroadcastReceivers(initializationIntent, 0); + for (ResolveInfo provider : providers) { + // send initialization broadcast, we send it directly instead of sending + // one broadcast for all plugins to wake up stopped plugins: + // http://developer.android.com/about/versions/android-3.1.html#launchcontrols + Intent intent = new Intent(); + intent.setClassName(provider.activityInfo.packageName, provider.activityInfo.name); + intent.setAction(initializationIntent.getAction()); + mContext.sendBroadcast(intent); + } + + return providers; + } + + @NonNull + private List getOnlineMapsFromPlugin(ResolveInfo provider) { + List sources = new ArrayList<>(); + + String authority; + try { + authority = PluginTileSourceContract.getProviderAuthority(mPackageManager, provider); + } catch (PluginTileSourceContractViolatedException | PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return sources; + } + + File cacheDir = new File(mContext.getExternalCacheDir(), "online"); + boolean useCache = cacheDir.mkdir() || cacheDir.isDirectory(); + + ContentProviderClient providerClient = mContext.getContentResolver().acquireContentProviderClient(authority); + if (providerClient == null) + return sources; + + List maps; + try { + maps = PluginTileSourceContract.Maps.getMaps(providerClient, authority); + } catch (RemoteException | PluginTileSourceContractViolatedException e) { + e.printStackTrace(); + return sources; + } + + for (PluginTileSourceContract.Maps.Metadata map : maps) { + PluginOnlineTileSource.Builder builder = PluginOnlineTileSource.builder(mContext); + builder.name(map.getName()); + builder.mapId(map.getIdentifier()); + builder.providerAuthority(authority); + + PluginOnlineTileSource source = builder.build(); + if (useCache) { + TileCache cache = new TileCache(mContext, cacheDir.getAbsolutePath(), authority + "/" + map.getIdentifier()); + source.setCache(cache); + } + sources.add(source); + } + providerClient.release(); + return sources; + } + + @NonNull + private List getOfflineMapsFromPlugin(ResolveInfo provider) { + List sources = new ArrayList<>(); + + String authority; + try { + authority = PluginTileSourceContract.getProviderAuthority(mPackageManager, provider); + } catch (PluginTileSourceContractViolatedException | PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return sources; + } + + ContentProviderClient providerClient = mContext.getContentResolver().acquireContentProviderClient(authority); + if (providerClient == null) + return sources; + + List maps; + try { + maps = PluginTileSourceContract.Maps.getMaps(providerClient, authority); + } catch (RemoteException | PluginTileSourceContractViolatedException e) { + e.printStackTrace(); + return sources; + } + + for (PluginTileSourceContract.Maps.Metadata map : maps) { + PluginOfflineTileSource.Builder builder = PluginOfflineTileSource.builder(mContext); + builder.name(map.getName()); + builder.mapId(map.getIdentifier()); + builder.providerAuthority(authority); + sources.add(builder.build()); + } + + providerClient.release(); + return sources; + } +}