package org.mtransit.android.provider; import java.util.Collection; import java.util.HashSet; import java.util.concurrent.TimeUnit; import org.json.JSONArray; import org.json.JSONObject; import org.mtransit.android.R; import org.mtransit.android.commons.ArrayUtils; import org.mtransit.android.commons.FileUtils; import org.mtransit.android.commons.LocationUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.PackageManagerUtils; import org.mtransit.android.commons.PreferenceUtils; import org.mtransit.android.commons.SqlUtils; import org.mtransit.android.commons.TimeUtils; import org.mtransit.android.commons.UriUtils; import org.mtransit.android.commons.data.AppStatus; import org.mtransit.android.commons.data.DefaultPOI; import org.mtransit.android.commons.data.POI; import org.mtransit.android.commons.data.POI.POIUtils; import org.mtransit.android.commons.data.POIStatus; import org.mtransit.android.commons.provider.AgencyProvider; import org.mtransit.android.commons.provider.ContentProviderConstants; import org.mtransit.android.commons.provider.POIProvider; import org.mtransit.android.commons.provider.POIProviderContract; import org.mtransit.android.commons.provider.StatusProvider; import org.mtransit.android.commons.provider.StatusProviderContract; import org.mtransit.android.data.Module; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.v4.util.ArrayMap; public class ModuleProvider extends AgencyProvider implements POIProviderContract, StatusProviderContract { private static final String TAG = ModuleProvider.class.getSimpleName(); @Override public String getLogTag() { return TAG; } @Override public String toString() { return getLogTag(); } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ private static final String PREF_KEY_LAST_UPDATE_MS = ModuleDbHelper.PREF_KEY_LAST_UPDATE_MS; private static final long MODULE_MAX_VALIDITY_IN_MS = TimeUnit.DAYS.toMillis(7); private static final long MODULE_VALIDITY_IN_MS = TimeUnit.DAYS.toMillis(1); private static final long MODULE_STATUS_MAX_VALIDITY_IN_MS = TimeUnit.MINUTES.toMillis(10); private static final long MODULE_STATUS_VALIDITY_IN_MS = TimeUnit.SECONDS.toMillis(30); private static final long MODULE_STATUS_VALIDITY_IN_FOCUS_IN_MS = TimeUnit.SECONDS.toMillis(15); private static final long MODULE_STATUS_MIN_DURATION_BETWEEN_REFRESH_IN_MS = TimeUnit.SECONDS.toMillis(20); private static final long MODULE_STATUS_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS = TimeUnit.SECONDS.toMillis(10); private static ModuleDbHelper dbHelper; private static int currentDbVersion = -1; private static UriMatcher uriMatcher = null; /** * Override if multiple {@link ModuleProvider} implementations in same app. */ private static UriMatcher getURIMATCHER(Context context) { if (uriMatcher == null) { uriMatcher = getNewUriMatcher(getAUTHORITY(context)); } return uriMatcher; } public static UriMatcher getNewUriMatcher(String authority) { UriMatcher URI_MATCHER = AgencyProvider.getNewUriMatcher(authority); StatusProvider.append(URI_MATCHER, authority); POIProvider.append(URI_MATCHER, authority); return URI_MATCHER; } private static String authority = null; /** * Override if multiple {@link ModuleProvider} implementations in same app. */ private static String getAUTHORITY(Context context) { if (authority == null) { authority = context.getResources().getString(R.string.module_authority); } return authority; } private static Uri authorityUri = null; /** * Override if multiple {@link ModuleProvider} implementations in same app. */ private static Uri getAUTHORITYURI(Context context) { if (authorityUri == null) { authorityUri = UriUtils.newContentUri(getAUTHORITY(context)); } return authorityUri; } @Override public boolean onCreateMT() { ping(); return true; } @Override public void ping() { } private ModuleDbHelper getDBHelper(Context context) { if (dbHelper == null) { // initialize dbHelper = getNewDbHelper(context); currentDbVersion = getCurrentDbVersion(); } else { // reset try { if (currentDbVersion != getCurrentDbVersion()) { dbHelper.close(); dbHelper = null; return getDBHelper(context); } } catch (Exception e) { // reset MTLog.w(this, e, "Can't check DB version!"); } } return dbHelper; } @Override public SQLiteOpenHelper getDBHelper() { return getDBHelper(getContext()); } @Override public Cursor queryMT(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { try { Cursor cursor = super.queryMT(uri, projection, selection, selectionArgs, sortOrder); if (cursor != null) { return cursor; } cursor = POIProvider.queryS(this, uri, selection); if (cursor != null) { return cursor; } cursor = StatusProvider.queryS(this, uri, selection); if (cursor != null) { return cursor; } throw new IllegalArgumentException(String.format("Unknown URI (query): '%s'", uri)); } catch (Exception e) { MTLog.w(this, e, "Error while resolving query '%s'!", uri); return null; } } @Override public String getSortOrder(Uri uri) { String sortOrder = POIProvider.getSortOrderS(this, uri); if (sortOrder != null) { return sortOrder; } sortOrder = StatusProvider.getSortOrderS(this, uri); if (sortOrder != null) { return sortOrder; } return super.getSortOrder(uri); } @Override public String getTypeMT(Uri uri) { String type = POIProvider.getTypeS(this, uri); if (type != null) { return type; } type = StatusProvider.getTypeS(this, uri); if (type != null) { return type; } return super.getTypeMT(uri); } @Override public int updateMT(Uri uri, ContentValues values, String selection, String[] selectionArgs) { MTLog.w(this, "The update method is not available."); return 0; } @Override public Uri insertMT(Uri uri, ContentValues values) { MTLog.w(this, "The insert method is not available."); return null; } @Override public int deleteMT(Uri uri, String selection, String[] selectionArgs) { MTLog.w(this, "The delete method is not available."); return 0; } @Override public Cursor getSearchSuggest(String query) { return ContentProviderConstants.EMPTY_CURSOR; // no search suggest for modules } @Override public ArrayMap<String, String> getSearchSuggestProjectionMap() { return null; // no search suggest for modules } @Override public String getSearchSuggestTable() { return null; // no search suggest for modules } @Override public Cursor getPOI(POIProviderContract.Filter poiFilter) { updateModuleDataIfRequired(); return getPOIFromDB(poiFilter); } @Override public Cursor getPOIFromDB(POIProviderContract.Filter poiFilter) { return POIProvider.getDefaultPOIFromDB(poiFilter, this); } @Override public long getPOIMaxValidityInMs() { return MODULE_MAX_VALIDITY_IN_MS; } @Override public long getPOIValidityInMs() { return MODULE_VALIDITY_IN_MS; } public void updateModuleDataIfRequired() { long lastUpdateInMs = PreferenceUtils.getPrefLcl(getContext(), PREF_KEY_LAST_UPDATE_MS, 0l); long nowInMs = TimeUtils.currentTimeMillis(); if (lastUpdateInMs + getPOIMaxValidityInMs() < nowInMs) { // too old to display? deleteAllModuleData(); updateAllModuleDataFromWWW(lastUpdateInMs); return; } if (lastUpdateInMs + getPOIValidityInMs() < nowInMs) { // try to refresh? updateAllModuleDataFromWWW(lastUpdateInMs); } } private int deleteAllModuleData() { int affectedRows = 0; try { affectedRows = getDBHelper(getContext()).getWritableDatabase().delete(ModuleDbHelper.T_MODULE, null, null); } catch (Exception e) { MTLog.w(this, e, "Error while deleting all module data!"); } return affectedRows; } private synchronized void updateAllModuleDataFromWWW(long oldLastUpdatedInMs) { if (PreferenceUtils.getPrefLcl(getContext(), PREF_KEY_LAST_UPDATE_MS, 0l) > oldLastUpdatedInMs) { return; // too late, another thread already updated } loadDataFromWWW(); } private HashSet<Module> loadDataFromWWW() { try { long newLastUpdateInMs = TimeUtils.currentTimeMillis(); int fileResId = R.raw.modules; String jsonString = FileUtils.fromFileRes(getContext(), fileResId); HashSet<Module> modules = new HashSet<Module>(); JSONArray jsonArray = new JSONArray(jsonString); for (int i = 0; i < jsonArray.length(); i++) { JSONObject jModule = jsonArray.getJSONObject(i); Module module = Module.fromSimpleJSONStatic(jModule, getAUTHORITY(getContext())); if (module == null) { continue; // error while converting JSON to Module } module.setId(i); modules.add(module); } deleteAllModuleData(); insertModulesLockDB(this, modules); PreferenceUtils.savePrefLcl(getContext(), PREF_KEY_LAST_UPDATE_MS, newLastUpdateInMs, true); // sync return modules; } catch (Exception e) { MTLog.w(this, e, "INTERNAL ERROR: Unknown Exception"); return null; } } private static synchronized int insertModulesLockDB(POIProviderContract provider, Collection<Module> defaultPOIs) { int affectedRows = 0; SQLiteDatabase db = null; try { db = provider.getDBHelper().getWritableDatabase(); db.beginTransaction(); // start the transaction if (defaultPOIs != null) { for (DefaultPOI defaultPOI : defaultPOIs) { long rowId = db.insert(provider.getPOITable(), POIProvider.POIDbHelper.T_POI_K_ID, defaultPOI.toContentValues()); if (rowId > 0) { affectedRows++; } } } db.setTransactionSuccessful(); // mark the transaction as successful } catch (Exception e) { MTLog.w(TAG, e, "ERROR while applying batch update to the database!"); } finally { SqlUtils.endTransaction(db); } return affectedRows; } @Override public POIStatus getNewStatus(StatusProviderContract.Filter filter) { if (filter == null || !(filter instanceof AppStatus.AppStatusFilter)) { MTLog.w(this, "getNewStatus() > Can't find new schecule whithout AppStatusFilter!"); return null; } AppStatus.AppStatusFilter moduleStatusFilter = (AppStatus.AppStatusFilter) filter; return getNewModuleStatus(moduleStatusFilter); } public POIStatus getNewModuleStatus(AppStatus.AppStatusFilter filter) { long newLastUpdateInMs = TimeUtils.currentTimeMillis(); boolean appInstalled = PackageManagerUtils.isAppInstalled(getContext(), filter.getPkg()); return new AppStatus(filter.getTargetUUID(), newLastUpdateInMs, getStatusMaxValidityInMs(), newLastUpdateInMs, appInstalled); } @Override public void cacheStatus(POIStatus newStatusToCache) { StatusProvider.cacheStatusS(this, newStatusToCache); } @Override public POIStatus getCachedStatus(StatusProviderContract.Filter statusFilter) { return StatusProvider.getCachedStatusS(this, statusFilter.getTargetUUID()); } @Override public boolean purgeUselessCachedStatuses() { return StatusProvider.purgeUselessCachedStatuses(this); } @Override public boolean deleteCachedStatus(int cachedStatusId) { return StatusProvider.deleteCachedStatus(this, cachedStatusId); } @Override public Uri getAuthorityUri() { return getAUTHORITYURI(getContext()); } @Override public String getStatusDbTableName() { return ModuleDbHelper.T_MODULE_STATUS; } @Override public boolean isAgencyDeployed() { return SqlUtils.isDbExist(getContext(), getDbName()); } @Override public boolean isAgencySetupRequired() { boolean setupRequired = false; if (currentDbVersion > 0 && currentDbVersion != getCurrentDbVersion()) { setupRequired = true; // live update required => update } else if (!SqlUtils.isDbExist(getContext(), getDbName())) { setupRequired = true; // not deployed => initialization } else if (SqlUtils.getCurrentDbVersion(getContext(), getDbName()) != getCurrentDbVersion()) { setupRequired = true; // update required => update } return setupRequired; } @Override public UriMatcher getAgencyUriMatcher() { return getURIMATCHER(getContext()); } @Override public int getStatusType() { return POI.ITEM_STATUS_TYPE_APP; } @Override public int getAgencyVersion() { return getCurrentDbVersion(); } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ @Override public int getAgencyLabelResId() { return R.string.module_label; } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ @Override public String getAgencyColorString(Context context) { return null; // default } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ @Override public int getAgencyShortNameResId() { return R.string.module_short_name; } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ @Override public LocationUtils.Area getAgencyArea(Context context) { return LocationUtils.THE_WORLD; } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ public String getDbName() { return ModuleDbHelper.DB_NAME; } @Override public UriMatcher getURI_MATCHER() { return getURIMATCHER(getContext()); } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ public int getCurrentDbVersion() { return ModuleDbHelper.getDbVersion(); } /** * Override if multiple {@link ModuleProvider} implementations in same app. */ public ModuleDbHelper getNewDbHelper(Context context) { return new ModuleDbHelper(context.getApplicationContext()); } @Override public long getStatusMaxValidityInMs() { return MODULE_STATUS_MAX_VALIDITY_IN_MS; } @Override public long getStatusValidityInMs(boolean inFocus) { if (inFocus) { return MODULE_STATUS_VALIDITY_IN_FOCUS_IN_MS; } return MODULE_STATUS_VALIDITY_IN_MS; } @Override public long getMinDurationBetweenRefreshInMs(boolean inFocus) { if (inFocus) { return MODULE_STATUS_MIN_DURATION_BETWEEN_REFRESH_IN_FOCUS_IN_MS; } return MODULE_STATUS_MIN_DURATION_BETWEEN_REFRESH_IN_MS; } private static ArrayMap<String, String> poiProjectionMap; @Override public ArrayMap<String, String> getPOIProjectionMap() { if (poiProjectionMap == null) { poiProjectionMap = getNewPoiProjectionMap(getAUTHORITY(getContext())); } return poiProjectionMap; } public static ArrayMap<String, String> getNewPoiProjectionMap(String authority) { // @formatter:off return SqlUtils.ProjectionMapBuilder.getNew() .appendValue(SqlUtils.concatenate(// SqlUtils.escapeString(POIUtils.UID_SEPARATOR), // SqlUtils.escapeString(authority), // SqlUtils.getTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_PKG) // ), POIProviderContract.Columns.T_POI_K_UUID_META) // .appendValue(Module.DST_ID, POIProviderContract.Columns.T_POI_K_DST_ID_META) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_ID, POIProviderContract.Columns.T_POI_K_ID) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_NAME, POIProviderContract.Columns.T_POI_K_NAME) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_LAT, POIProviderContract.Columns.T_POI_K_LAT) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_LNG, POIProviderContract.Columns.T_POI_K_LNG) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_TYPE, POIProviderContract.Columns.T_POI_K_TYPE) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_STATUS_TYPE, POIProviderContract.Columns.T_POI_K_STATUS_TYPE) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_ACTIONS_TYPE, POIProviderContract.Columns.T_POI_K_ACTIONS_TYPE) // // .appendTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_PKG, ModuleColumns.T_MODULE_K_PKG) // .appendTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_TARGET_TYPE_ID, ModuleColumns.T_MODULE_K_TARGET_TYPE_ID) // .appendTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_COLOR, ModuleColumns.T_MODULE_K_COLOR) // .appendTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_LOCATION, ModuleColumns.T_MODULE_K_LOCATION) // .appendTableColumn(ModuleDbHelper.T_MODULE, ModuleDbHelper.T_MODULE_K_NAME_FR, ModuleColumns.T_MODULE_K_NAME_FR) // .build(); // @formatter:on } public static final String[] PROJECTION_MODULE = new String[] { ModuleColumns.T_MODULE_K_PKG, ModuleColumns.T_MODULE_K_TARGET_TYPE_ID, ModuleColumns.T_MODULE_K_COLOR, ModuleColumns.T_MODULE_K_LOCATION, ModuleColumns.T_MODULE_K_NAME_FR }; public static final String[] PROJECTION_MODULE_POI = ArrayUtils.addAll(POIProvider.PROJECTION_POI, PROJECTION_MODULE); @Override public String[] getPOIProjection() { return PROJECTION_MODULE_POI; } @Override public String getPOITable() { return ModuleDbHelper.T_MODULE; } public static class ModuleColumns { public static final String T_MODULE_K_PKG = POIProviderContract.Columns.getFkColumnName("pkg"); public static final String T_MODULE_K_TARGET_TYPE_ID = POIProviderContract.Columns.getFkColumnName("targetTypeId"); public static final String T_MODULE_K_COLOR = POIProviderContract.Columns.getFkColumnName("color"); public static final String T_MODULE_K_LOCATION = POIProviderContract.Columns.getFkColumnName("location"); public static final String T_MODULE_K_NAME_FR = POIProviderContract.Columns.getFkColumnName("name_fr"); } }