package me.ccrama.redditslide; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.os.AsyncTask; import android.os.Environment; import android.widget.Toast; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.MaterialDialog; import com.nostra13.universalimageloader.cache.disc.DiskCache; import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiskCache; import com.nostra13.universalimageloader.cache.disc.impl.ext.LruDiskCache; import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator; import com.nostra13.universalimageloader.core.DisplayImageOptions; import com.nostra13.universalimageloader.core.ImageLoader; import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; import com.nostra13.universalimageloader.core.assist.ImageScaleType; import com.nostra13.universalimageloader.core.assist.ImageSize; import net.dean.jraw.http.HttpRequest; import net.dean.jraw.http.MediaTypes; import net.dean.jraw.http.RestResponse; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import me.ccrama.redditslide.Activities.SendMessage; import me.ccrama.redditslide.util.LogUtil; import me.ccrama.redditslide.util.OkHttpImageDownloader; /** * Created by Carlos on 4/15/2017. */ public class ImageFlairs { public static void syncFlairs(final Context context, final String subreddit) { new StylesheetFetchTask(subreddit, context) { @Override protected void onPostExecute(FlairStylesheet flairStylesheet) { super.onPostExecute(flairStylesheet); d.dismiss(); if (flairStylesheet != null) { flairs.edit().putBoolean(subreddit.toLowerCase(), true).commit(); d = new AlertDialogWrapper.Builder(context).setTitle("Subreddit flairs synced") .setMessage("Slide found and synced " + flairStylesheet.count + " image flairs") .setPositiveButton(R.string.btn_ok, null) .show(); } else { AlertDialogWrapper.Builder b = new AlertDialogWrapper.Builder(context).setTitle( "Error syncing subreddit flairs") .setMessage("Slide could not find any subreddit flairs to sync from /r/" + subreddit + "'s stylesheet.") .setPositiveButton(R.string.btn_ok, null); if(Authentication.isLoggedIn){ b.setNeutralButton("Report no flairs", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Toast.makeText(context, "Not all subreddits can be parsed, but send a message to SlideBot and hopefully we can add support for this subreddit :)\n\nPlease, only send one report.", Toast.LENGTH_LONG); Intent i = new Intent(context, SendMessage.class); i.putExtra(SendMessage.EXTRA_NAME, "slidebot"); i.putExtra(SendMessage.EXTRA_MESSAGE, "/r/" + subreddit); i.putExtra(SendMessage.EXTRA_REPLY, "Subreddit flair"); context.startActivity(i); } }); } d = b.show(); } } @Override protected void onPreExecute() { d = new MaterialDialog.Builder(context).progress(true, 100) .content(R.string.misc_please_wait) .title("Syncing flairs...") .cancelable(false) .show(); } }.execute(); } static class StylesheetFetchTask extends AsyncTask<Void, Void, FlairStylesheet> { String subreddit; Context context; Dialog d; StylesheetFetchTask(String subreddit, Context context) { super(); this.context = context; this.subreddit = subreddit; } @Override protected FlairStylesheet doInBackground(Void... params) { try { HttpRequest r = new HttpRequest.Builder().host("reddit.com") .path("/r/" + subreddit + "/stylesheet") .expected(MediaTypes.CSS.type()) .build(); RestResponse response = Authentication.reddit.execute(r); String stylesheet = response.getRaw(); ArrayList<String> allImages = new ArrayList<>(); FlairStylesheet flairStylesheet = new FlairStylesheet(stylesheet); int count = 0; for (String s : flairStylesheet.getListOfFlairIds()) { String classDef = flairStylesheet.getClass(flairStylesheet.stylesheetString, "flair-" + s); try { String backgroundURL = flairStylesheet.getBackgroundURL(classDef); if (backgroundURL == null) backgroundURL = flairStylesheet.defaultURL; if (!allImages.contains(backgroundURL)) allImages.add(backgroundURL); } catch (Exception e) { // e.printStackTrace(); } } if (flairStylesheet.defaultURL != null) { LogUtil.v("Default url is " + flairStylesheet.defaultURL); allImages.add(flairStylesheet.defaultURL); } for (String backgroundURL : allImages) { flairStylesheet.cacheFlairsByFile(subreddit, backgroundURL, context); } return flairStylesheet; } catch (Exception e) { e.printStackTrace(); return null; } } } public static SharedPreferences flairs; public static boolean isSynced(String subreddit) { return flairs.contains(subreddit.toLowerCase()); } public static class CropTransformation { private int width, height, x, y; private String id; public CropTransformation(Context context, String id, int width, int height, int x, int y) { super(); this.id = id; this.width = width; this.height = height; this.x = x; this.y = y; } public Bitmap transform(Bitmap bitmap, boolean isPercentage) throws Exception { int nX, nY; if (isPercentage) { nX = Math.max(0, Math.min(bitmap.getWidth() - 1, bitmap.getWidth() * x / 100)); nY = Math.max(0, Math.min(bitmap.getHeight() - 1, bitmap.getHeight() * y / 100)); } else { nX = Math.max(0, Math.min(bitmap.getWidth() - 1, x)); nY = Math.max(0, Math.min(bitmap.getHeight() - 1, y)); } int nWidth = Math.max(1, Math.min(bitmap.getWidth() - nX - 1, width)), nHeight = Math.max(1, Math.min(bitmap.getHeight() - nY - 1, height)); LogUtil.v("Flair loaded: " + id + " size: " + nWidth + "x" + nHeight + " location: " + nX + ":" + nY + " and bit is " + bitmap.getWidth() + ":" + bitmap.getHeight()); Bitmap b = Bitmap.createBitmap(bitmap, nX, nY, nWidth, nHeight); return b; } } static class FlairStylesheet { String stylesheetString; Dimensions defaultDimension = new Dimensions(); Location defaultLocation = new Location(); String defaultURL = ""; int count; Dimensions prevDimension = null; class Dimensions { int width, height; Boolean scale = false; Boolean missing = true; Dimensions(int width, int height) { this.width = width; this.height = height; if (height == -1) { scale = true; } missing = false; } Dimensions() { } } class Location { int x, y; Boolean isPercentage = false; Boolean missing = true; Location(int x, int y) { this.x = x; this.y = y; missing = false; } Location(int x, int y, boolean isPercentage) { this.x = x; this.y = y; this.isPercentage = isPercentage; missing = false; } Location() { } } FlairStylesheet(String stylesheetString) { stylesheetString = stylesheetString.replaceAll("@media[^{]+\\{([\\s\\S]+?\\})\\s*\\}", ""); stylesheetString = stylesheetString.replaceAll("~.", " ."); this.stylesheetString = stylesheetString; String baseFlairDef = getClass(stylesheetString, "flair"); if (baseFlairDef == null) return; LogUtil.v("Base is " + baseFlairDef); // Attempts to find default dimension, offset and image URL defaultDimension = getBackgroundSize(baseFlairDef); LogUtil.v("Default dimens are " + defaultDimension.width + ":" + defaultDimension.height); defaultLocation = getBackgroundPosition(baseFlairDef); defaultURL = getBackgroundURL(baseFlairDef); count = 0; } /** * Get class definition string by class name. * * @param cssDefinitionString * @param className * @return */ String getClass(String cssDefinitionString, String className) { Pattern propertyDefinition = Pattern.compile( "(?<! )\\." + className + "(?!-|\\[|[A-Za-z0-9_.])([^\\{]*)*\\{(.+?)\\}"); Matcher matches = propertyDefinition.matcher(cssDefinitionString); String properties = null; while (matches.find()) { if (properties == null) properties = ""; properties = matches.group(2) + ";" + properties; // append properties to simulate property overriding } return properties; } /** * Get property value inside a class definition by property name. * * @param classDefinitionsString * @param property * @return */ String getProperty(String classDefinitionsString, String property) { Pattern propertyDefinition = Pattern.compile("(?<!-)" + property + "\\s*:\\s*(.+?)(;|$)"); Matcher matches = propertyDefinition.matcher(classDefinitionsString); if (matches.find()) { return matches.group(1); } else { return null; } } //Attempts to get a real integer value instead of "auto", if possible String getPropertyTryNoAuto(String classDefinitionsString, String property) { Pattern propertyDefinition = Pattern.compile("(?<!-)" + property + "\\s*:\\s*(.+?)(;|$)"); Matcher matches = propertyDefinition.matcher(classDefinitionsString); String defaultString; if (matches.find()) { defaultString = matches.group(1); } else { return null; } LogUtil.v("Has auto"); while((defaultString.contains("auto")||(!defaultString.contains("%") || !defaultString.contains("px"))) && matches.find()){ defaultString = matches.group(1); } LogUtil.v("Returning " + defaultString); return defaultString; } String getPropertyBackgroundUrl(String classDefinitionsString) { Pattern propertyDefinition = Pattern.compile("background:url\\([\"'](.+?)[\"']\\)"); Matcher matches = propertyDefinition.matcher(classDefinitionsString); if (matches.find()) { return matches.group(1); } else { return null; } } /** * Get flair background url in class definition. * * @param classDefinitionString * @return */ String getBackgroundURL(String classDefinitionString) { Pattern urlDefinition = Pattern.compile("url\\([\"\'](.+?)[\"\']\\)"); String backgroundProperty = getPropertyBackgroundUrl(classDefinitionString); if (backgroundProperty != null) { // check "background" String url = backgroundProperty; if (url.startsWith("//")) url = "https:" + url; return url; } // either backgroundProperty is null or url cannot be found String backgroundImageProperty = getProperty(classDefinitionString, "background-image"); if (backgroundImageProperty != null) { // check "background-image" Matcher matches = urlDefinition.matcher(backgroundImageProperty); if (matches.find()) { String url = matches.group(1); if (url.startsWith("//")) url = "https:" + url; return url; } } // could not find any background url return null; } /** * Get background dimension in class definition. * * @param classDefinitionString * @return */ Dimensions getBackgroundSize(String classDefinitionString) { Pattern numberDefinition = Pattern.compile("(\\d+)\\s*px"); boolean autoWidth = false, autoHeight = false; // check common properties used to define width String widthProperty = getPropertyTryNoAuto(classDefinitionString, "width"); if (widthProperty == null) { widthProperty = getPropertyTryNoAuto(classDefinitionString, "min-width"); } else if (widthProperty.equals("auto")) { autoWidth = true; } if (widthProperty == null) { widthProperty = getProperty(classDefinitionString, "text-indent"); } if (widthProperty == null) return new Dimensions(); // check common properties used to define height String heightProperty = getPropertyTryNoAuto(classDefinitionString, "height"); if (heightProperty == null) { heightProperty = getPropertyTryNoAuto(classDefinitionString, "min-height"); } else if (heightProperty.equals("auto")) { autoHeight = true; } if (heightProperty == null) return new Dimensions(); int width = 0, height = 0; Matcher matches; if (!autoWidth) { matches = numberDefinition.matcher(widthProperty); if (matches.find()) { width = Integer.parseInt(matches.group(1)); } else { return new Dimensions(); } } if (!autoHeight) { matches = numberDefinition.matcher(heightProperty); if (matches.find()) { height = Integer.parseInt(matches.group(1)); } else { return new Dimensions(); } } if (autoWidth) { width = height; } if (autoHeight) { height = width; } return new Dimensions(width, height); } /** * Get background scaling in class definition. * * @param classDefinitionString * @return */ Dimensions getBackgroundScaling(String classDefinitionString) { Pattern positionDefinitionPx = Pattern.compile("([+-]?\\d+|0)(px\\s|\\s)+(|([+-]?\\d+|0)(px|))"); String backgroundPositionProperty = getProperty(classDefinitionString, "background-size"); String backgroundPositionPropertySecondary = getProperty(classDefinitionString, "background-size"); if (backgroundPositionProperty == null && backgroundPositionPropertySecondary == null || backgroundPositionProperty == null && !backgroundPositionPropertySecondary.contains("px ") && !backgroundPositionPropertySecondary.contains("px;")) { return new Dimensions(); } Matcher matches = positionDefinitionPx.matcher(backgroundPositionProperty); if (matches.find()) { return new Dimensions(Integer.parseInt(matches.group(1)), matches.groupCount() < 2 ? Integer.parseInt(matches.group(3)) : -1); } else { return new Dimensions(); } } /** * Get background offset in class definition. * * @param classDefinitionString * @return */ Location getBackgroundPosition(String classDefinitionString) { Pattern positionDefinitionPx = Pattern.compile("([+-]?\\d+|0)(px\\s|\\s)+([+-]?\\d+|0)(px|)"), positionDefinitionPercentage = Pattern.compile("([+-]?\\d+|0)(%\\s|\\s)+([+-]?\\d+|0)(%|)"); String backgroundPositionProperty = getProperty(classDefinitionString, "background-position"); if (backgroundPositionProperty == null) { backgroundPositionProperty = getProperty(classDefinitionString, "background"); if (backgroundPositionProperty == null) { return new Location(); } } Matcher matches = positionDefinitionPx.matcher(backgroundPositionProperty); try { if (matches.find()) { return new Location(-Integer.parseInt(matches.group(1)), -Integer.parseInt(matches.group(3))); } else { matches = positionDefinitionPercentage.matcher(backgroundPositionProperty); if (matches.find()) { return new Location( Integer.parseInt(matches.group(1)), Integer.parseInt(matches.group(3)), true); } } } catch (NumberFormatException ignored) { } return new Location(); } Dimensions getBackgroundOffset(String classDefinitionString) { Pattern positionDefinitionPx = Pattern.compile("([+-]?\\d+|0)\\/+([+-]?\\d+|0)(px|)"); String backgroundPositionProperty = getProperty(classDefinitionString, "background"); if (backgroundPositionProperty == null) { return new Dimensions(); } Matcher matches = positionDefinitionPx.matcher(backgroundPositionProperty); try { if (matches.find()) { return new Dimensions(Integer.parseInt(matches.group(2)), Integer.parseInt(matches.group(2))); } } catch (NumberFormatException ignored) { } return new Dimensions(); } /** * Request a flair by flair id. `.into` can be chained onto this method call. * * @param id * @param context * @return */ void cacheFlairsByFile(final String sub, final String filename, final Context context) { final ArrayList<String> flairsToGet = new ArrayList<>(); LogUtil.v("Doing sheet " + filename); for (String s : getListOfFlairIds()) { String classDef = getClass(stylesheetString, "flair-" + s); if (classDef != null && !classDef.isEmpty()) { String backgroundURL = getBackgroundURL(classDef); if (backgroundURL == null) backgroundURL = defaultURL; if (backgroundURL != null && backgroundURL.equalsIgnoreCase(filename)) { flairsToGet.add(s); } } } String scaling = getClass(stylesheetString, "flair"); final Dimensions backScaling; final Dimensions offset; if (scaling != null) { backScaling = getBackgroundScaling(scaling); offset = getBackgroundOffset(scaling); LogUtil.v("Offset is " + offset.width); } else { backScaling = new Dimensions(); offset = new Dimensions(); } if ((!backScaling.missing && !backScaling.scale) || (!offset.missing && !offset.scale)) { Bitmap loaded = getFlairImageLoader(context).loadImageSync(filename, new ImageSize(backScaling.width, backScaling.height)); if (loaded != null) { Bitmap b; if(backScaling.missing || backScaling.width < offset.width) { b = Bitmap.createScaledBitmap(loaded, offset.width, offset.height, false); } else { b = Bitmap.createScaledBitmap(loaded, backScaling.width, backScaling.height, false); } loadingComplete(b, sub, context, filename, flairsToGet); loaded.recycle(); } } else { Bitmap loadedB = getFlairImageLoader(context).loadImageSync(filename); if (loadedB != null) { if (backScaling.scale) { int width = backScaling.width; int height = loadedB.getHeight(); int scaledHeight = (height * width) / loadedB.getWidth(); loadingComplete( Bitmap.createScaledBitmap(loadedB, width, scaledHeight, false), sub, context, filename, flairsToGet); loadedB.recycle(); } else { loadingComplete(loadedB, sub, context, filename, flairsToGet); } } } } private void loadingComplete(Bitmap loadedImage, String sub, Context context, String filename, ArrayList<String> flairsToGet) { if (loadedImage != null) { for (String id : flairsToGet) { Bitmap newBit = null; String classDef = FlairStylesheet.this.getClass(stylesheetString, "flair-" + id); if (classDef == null) break; Dimensions flairDimensions = getBackgroundSize(classDef); if (flairDimensions.missing) { flairDimensions = defaultDimension; } prevDimension = flairDimensions; Location flairLocation = getBackgroundPosition(classDef); if (flairLocation.missing) flairLocation = defaultLocation; LogUtil.v("Flair: " + id + " size: " + flairDimensions.width + "x" + flairDimensions.height + " location: " + flairLocation.x + ":" + flairLocation.y); try { newBit = new CropTransformation(context, id, flairDimensions.width, flairDimensions.height, flairLocation.x, flairLocation.y).transform( loadedImage, flairLocation.isPercentage); } catch (Exception e) { e.printStackTrace(); } try { getFlairImageLoader(context).getDiskCache() .save(sub.toLowerCase() + ":" + id.toLowerCase(), newBit); count += 1; } catch (Exception e) { e.printStackTrace(); } } loadedImage.recycle(); } else { LogUtil.v("Loaded image is null for " + filename); } } /** * Util function * * @return */ List<String> getListOfFlairIds() { Pattern flairId = Pattern.compile("\\.flair-(\\w+)\\s*(\\{|\\,|\\:|)"); Matcher matches = flairId.matcher(stylesheetString); List<String> flairIds = new ArrayList<>(); while (matches.find()) { if (!flairIds.contains(matches.group(1))) flairIds.add(matches.group(1)); } Collections.sort(flairIds); return flairIds; } } public static class FlairImageLoader extends ImageLoader { private volatile static FlairImageLoader instance; /** Returns singletone class instance */ public static FlairImageLoader getInstance() { if (instance == null) { synchronized (ImageLoader.class) { if (instance == null) { instance = new FlairImageLoader(); } } } return instance; } } public static FlairImageLoader getFlairImageLoader(Context context) { if (imageLoader == null) { return initFlairImageLoader(context); } else { return imageLoader; } } public static FlairImageLoader imageLoader; public static File getCacheDirectory(Context context) { if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) && context.getExternalCacheDir() != null) { return new File(context.getExternalCacheDir(), "flairs"); } return new File(context.getCacheDir(), "flairs"); } public static FlairImageLoader initFlairImageLoader(Context context) { long discCacheSize = 1024 * 1024 * 100; //100 MB limit DiskCache discCache; File dir = getCacheDirectory(context); int threadPoolSize; discCacheSize *= 100; threadPoolSize = 7; if (discCacheSize > 0) { try { dir.mkdir(); discCache = new LruDiskCache(dir, new Md5FileNameGenerator(), discCacheSize); } catch (IOException e) { discCache = new UnlimitedDiskCache(dir); } } else { discCache = new UnlimitedDiskCache(dir); } options = new DisplayImageOptions.Builder().cacheOnDisk(true) .imageScaleType(ImageScaleType.NONE) .cacheInMemory(false) .resetViewBeforeLoading(false) .build(); ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context).threadPoolSize(threadPoolSize) .denyCacheImageMultipleSizesInMemory() .diskCache(discCache) .threadPoolSize(4) .imageDownloader(new OkHttpImageDownloader(context)) .defaultDisplayImageOptions(options) .build(); if (FlairImageLoader.getInstance().isInited()) { FlairImageLoader.getInstance().destroy(); } imageLoader = FlairImageLoader.getInstance(); imageLoader.init(config); return imageLoader; } public static DisplayImageOptions options; }