package net.i2p.android.router.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import net.i2p.android.apps.EepGetFetcher;
import net.i2p.android.router.BuildConfig;
import net.i2p.android.router.util.AppCache;
import net.i2p.android.router.util.Util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Usage: content://net.i2p.android.router/NONCE/ENCODED-SCHEME/ENCODED-AUTHORITY/ENCODED_PATH + QUERY_MARKER + ENCODED-QUERY
* Where NONCE is set at instantiation
* Do not include the fragment.
*
* http://www.techjini.com/blog/2009/01/10/android-tip-1-contentprovider-accessing-local-file-system-from-webview-showing-image-in-webview-using-content/
*
* ===================
*
* quote:
* http://stackoverflow.com/questions/4616675/how-does-a-contentresolver-locate-the-corresponding-contentprovider
*
* There is no way to see if the ContentProvider is running. It is started and stopped automatically
* by ContentResolver as needed. When you start making requests for a specific contentAuthority,
* the associated provider will be started if it isn't already running. It will be stopped automatically
* by ContentResolver, some time later once it has sat idle and it looks like it might not be needed for a while.
*
*/
public class CacheProvider extends ContentProvider {
// FIXME not persistent, use SharedPrefs
/** content:// Uri to absolute path of the file */
private SharedPreferences _sharedPrefs;
private static final String SHARED_PREFS = "net.i2p.android.router.provider.CacheProvider";
//private static final String NONCE = Integer.toString(Math.abs((new java.util.Random()).nextInt()));
private static final String NONCE = "0";
private static final String SCHEME = "content";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".provider";
/** includes the nonce */
public static final Uri CONTENT_URI = Uri.parse(SCHEME + "://" + AUTHORITY + '/' + NONCE);
/** the database keys */
public static final String DATA = "_data";
public static final String CURRENT_BASE = "currentBase";
private static final String QUERY_MARKER = "!!QUERY!!";
private static final String ERROR_HEADER = "<html><head><title>Not Found</title></head><body>";
private static final String ERROR_URL = "<p>Unable to load URL: ";
private static final String ERROR_ROUTER = "<p>Your router does not appear to be up.</p>";
private static final String ERROR_FOOTER = "</body></html>";
/**
* Generate a cache content (resource) URI for a given URI key
* If the key is already a content URI, canonicalize it
* by twizzling the query if necessary
*
* @param key must contain a scheme, authority and path
* @return null on error
*/
public static Uri getContentUri(Uri key) {
String s = key.getScheme();
String a = key.getEncodedAuthority();
String p = key.getEncodedPath();
if (s == null || a == null || p == null)
return null;
String q = key.getEncodedQuery();
// canonicalize resource URI
if (s.equals(SCHEME)) {
if (q == null || !a.equals(AUTHORITY))
return key;
if (p.contains(QUERY_MARKER)) {
Util.d("Key contains both queries ?!? " + key);
return null;
}
// twizzle query
StringBuilder buf = new StringBuilder(128);
buf.append(s).append("://")
.append(a);
if (!p.startsWith("/"))
buf.append('/');
buf.append(p);
buf.append(QUERY_MARKER).append(q);
return Uri.parse(buf.toString());
}
// convert http URI to resource
StringBuilder buf = new StringBuilder(128);
buf.append(CONTENT_URI).append('/')
.append(s).append('/')
.append(a);
if (!p.startsWith("/"))
buf.append('/');
buf.append(p);
if (q != null)
buf.append(QUERY_MARKER).append(q);
return Uri.parse(buf.toString());
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
Util.d("CacheProvider open " + uri);
// if uri is malformed and we have a current base, rectify it
uri = rectifyContentUri(getCurrentBase(), uri);
// map the resource URI to a local file URI and return it if it exists
String filePath = get(uri);
if (filePath != null) {
try {
File file = new File(filePath);
if (file.exists())
Util.d("CacheProvider returning " + file);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
} catch (FileNotFoundException fnfe) {
Util.d("CacheProvider not found", fnfe);
remove(uri);
}
}
Util.d("CacheProvider not in cache " + uri);
Uri newUri = getI2PUri(uri);
Util.d("CacheProvider fetching: " + newUri);
return eepFetch(newUri);
}
/**
* Generate an i2p URI for a content URI
*
* @param uri must contain a scheme, authority and path with nonce etc. as defined above
* @return non-null
* @throws java.io.FileNotFoundException on error
*/
public static Uri getI2PUri(Uri uri) throws FileNotFoundException {
String resPath = uri.getEncodedPath();
if (resPath == null)
throw new FileNotFoundException("Bad uri no path? " + uri);
String[] segs = resPath.split("/", 5);
// first seg is empty since string starts with /
String nonce = segs.length > 1 ? segs[1] : null;
String scheme = segs.length > 2 ? segs[2] : null;
String host = segs.length > 3 ? segs[3].toLowerCase() : null;
String realPath = segs.length > 4 ? segs[4] : "";
String query = uri.getEncodedQuery();
if (query == null) {
int marker = realPath.indexOf(QUERY_MARKER);
if (marker >= 0) {
query = realPath.substring(marker + QUERY_MARKER.length());
realPath = realPath.substring(0, marker);
}
}
String debug = "CacheProvider nonce: " + nonce + " scheme: " + scheme + " host: " + host + " realPath: " + realPath + " query: " + query;
Util.d(debug);
if ((!NONCE.equals(nonce)) ||
(!"http".equals(scheme)) ||
(host == null) ||
(!host.endsWith(".i2p")))
throw new FileNotFoundException(debug);
String i2pUri = scheme + "://" + host + '/' + realPath;
if (query != null)
i2pUri += '?' + query;
return Uri.parse(i2pUri);
}
/**
* Rectify a malformed content uri using the current base content uri.
* Any query in uri is also canonicalized.
*
* @param base a valid content base uri e.g. content://net.i2p.android.router/0/http/bar.i2p/baz/baf.html
* if null, uri is returned.
* @param uri a malformed content uri e.g. content://net.i2p.android.router/foo.html
* @return a valid content uri e.g. content://net.i2p.android.router/0/http/bar.i2p/foo.html,
* or the original uri on error, or if no rectification needed
*/
public static Uri rectifyContentUri(Uri base, Uri uri) {
Util.d("rectifyContentUri base: " + base + " and uri: " + uri);
if (base == null)
return uri;
if (!SCHEME.equals(base.getScheme()))
return uri;
if (!AUTHORITY.equals(base.getEncodedAuthority()))
return uri;
String basePath = base.getEncodedPath();
if (basePath == null)
return uri;
String[] segs = basePath.split("/", 5);
if (segs.length < 3)
return uri;
// first seg is empty since string starts with /
if (!segs[1].equals(NONCE))
return uri;
if (!segs[2].equals("http"))
return uri;
String host = segs[3];
if (!SCHEME.equals(uri.getScheme()))
return uri;
if (!AUTHORITY.equals(uri.getEncodedAuthority()))
return uri;
String path = uri.getEncodedPath();
if (path != null && (path.startsWith(NONCE + '/') || path.startsWith('/' + NONCE + '/')))
return uri;
String query = uri.getEncodedQuery();
StringBuilder buf = new StringBuilder(128);
buf.append(SCHEME).append("://")
.append(AUTHORITY).append('/')
.append(NONCE).append("/http/")
.append(host);
if (path == null || !path.startsWith("/"))
buf.append('/');
if (path != null)
buf.append(path);
if (query != null)
buf.append(QUERY_MARKER).append(query);
Util.d("rectified from base: " + base + " and uri: " + uri + " to: " + buf);
return Uri.parse(buf.toString());
}
private ParcelFileDescriptor eepFetch(Uri uri) throws FileNotFoundException {
AppCache cache = AppCache.getInstance(getContext());
OutputStream out;
try {
out = cache.createCacheFile(uri);
} catch (IOException ioe) {
throw new FileNotFoundException(ioe.toString());
}
// in this constructor we don't use the error output, for now
EepGetFetcher fetcher = new EepGetFetcher(uri.toString(), out, false);
boolean success = fetcher.fetch();
if (success) {
File file = cache.getCacheFile(uri);
if (file.length() > 0) {
// this call will insert it back to us (don't set as current base)
Uri content = cache.addCacheFile(uri, false);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
} else {
Util.d("CacheProvider Sucess but no data " + uri);
}
} else {
Util.d("CacheProvider Eepget fail " + uri);
}
AppCache.getInstance().removeCacheFile(uri);
throw new FileNotFoundException("eepget fail");
}
public int delete(Uri uri, String selection, String[] selectionArgs) {
Util.d("CacheProvider delete " + uri);
boolean deleted = remove(uri);
return deleted ? 1 : 0;
}
public String getType(Uri uri) {
Util.d("CacheProvider getType " + uri);
return "text/html";
}
/*
* _data -> String absolute path of the file (NOT a file:// URI)
*/
public Uri insert(Uri uri, ContentValues values) {
String fileURI = values.getAsString(DATA);
if (fileURI != null) {
Util.d("CacheProvider insert " + uri);
put(uri, fileURI);
}
Boolean setAsCurrentBase = values.getAsBoolean(CURRENT_BASE);
if (setAsCurrentBase != null && setAsCurrentBase) {
Util.d("CacheProvider set current base " + uri);
setCurrentBase(uri);
}
return uri;
}
public boolean onCreate() {
_sharedPrefs = getContext().getSharedPreferences(SHARED_PREFS, 0);
cleanup();
return true;
}
public Cursor query(Uri uri, String[] projection, String selection, String[] selecctionArgs, String sortOrder) {
Util.d("CacheProvider query " + uri);
// TODO return a MatrixCursor with a _data entry
return null;
}
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
///// Map stuff
private void cleanup() {
String pfx = CONTENT_URI.toString();
List<String> toDelete = new ArrayList<>();
Map<String, ?> map = _sharedPrefs.getAll();
for (Map.Entry<String, ?> e : map.entrySet()) {
String path = (String) e.getValue();
if (!path.startsWith(pfx))
continue;
File f = new File(path);
if (!f.exists())
toDelete.add(e.getKey());
}
if (!toDelete.isEmpty()) {
SharedPreferences.Editor edit = _sharedPrefs.edit();
for (String key : toDelete) {
edit.remove(key);
}
edit.commit();
}
}
private String get(Uri uri) {
return getPref(uri.toString());
}
private void put(Uri uri, String fileURI) {
setPref(uri.toString(), fileURI);
}
/** @return may be null */
private Uri getCurrentBase() {
String url = getPref(CURRENT_BASE);
if (url != null)
return Uri.parse(url);
return null;
}
private void setCurrentBase(Uri contentURI) {
setPref(CURRENT_BASE, contentURI.toString());
}
/** @return true if it was removed */
private boolean remove(Uri uri) {
String old = getPref(uri.toString());
boolean success = deletePref(uri.toString());
return success && old != null;
}
/** @return null if not found */
private String getPref(String pref) {
return _sharedPrefs.getString(pref, null);
}
/** @return success */
private boolean setPref(String pref, String val) {
SharedPreferences.Editor edit = _sharedPrefs.edit();
edit.putString(pref, val);
return edit.commit();
}
/** @return success */
private boolean deletePref(String pref) {
SharedPreferences.Editor edit = _sharedPrefs.edit();
edit.remove(pref);
return edit.commit();
}
}