Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create downloadable custom theme and apply it during run time

I'm making an Android app that needs to allow client to maintain the resources from their server which would include strings, drawables etc.

I've already created a mechanism for downloading a zip file with all these files, and they're able to change strings pretty easy, I've also created a mechanism that allows the client to change bg color for UI controls, to change width, height etc but I have a feeling that there must be a better way to create all this.

So I believe the real question is:

What's the best practice to create a custom theme, deploy it on server, make the app download it, and apply it to app afterwards?

I know how to create custom theme and how to deploy it with the app, and how to apply it during runtime, but the problem here is that resources are pre-compiled and once you create APK there's no way for developer to change them which would be required in order to add new themes/drawables/styles/strings.

Do I need to create a custom mechanism for all this (loading images, styles, strings etc from the file system) and to apply them during runtime by creating my own controls that would do that in constructor for example or is there a way to do this properly :)? ( how does Swiftkey do this with all the keyboard themes, and how do similar apps do it allowing the users to download theme and apply it after that )?

I'm sorry if I didn't see similar question, I really tried to find an answer myself during past 2 days, but I failed to find anything useful, so this is my last chance to get a constructive answer :).

The closest to solution i need was this answer: Changing app theme at runtime using using external theme file but I've already made that functionality, and i know i can change colors like that, but the problem is that i would like to be able to change things like borders, on button pressed state etc that require resources other than simple color value :(.

Thanks heaps!

P.S. I've also read about the extension files so is that something i need to consider while thinking about this, or do i need to look elsewhere? The problem with obb files is that they must be deployed over PlayStore and that's not "perfect" for the client because they need to pack it by using jobb, and to deploy it to PlayStore which is too technical for them, so they would prefer creating a zip file, putting it on server, and the app should do the rest :).

like image 311
Bojan Kopanja Avatar asked Jul 30 '14 08:07

Bojan Kopanja


1 Answers

I've finally decided to solve this by making a custom system for handling drawables, strings etc so now i have a custom class called "ResourceManager" that handles what needs to be loadad and how, and themes are distributed as a zip file which app downloads, extracts and later uses.

I had to compile nine patch images by myself before putting them in zip file, and I did that using "abrc" from here: http://forum.xda-developers.com/showthread.php?t=785012

I've also created a simple bash script that goes recursively through custom folder and compiles all nine patch images with abrc.

I've also created a simple helper in the ResourceManager that checks and tells me the screen density so i can normally support images in hdpi, xhdpi etc densities, and finally i don't re-create the images every time i need them, i save them in a static list of HashMap so i can reuse the ones I've already created and that way i hope to prevent wasting too much phone's memory :).

OK that's all in short lines, if anyone has any questions please let me know, i'll be glad to share this experience with anyone.

Cheers!

============ EDIT ============

Here's the class I ended up writing for this purpose (it downloads the file, checks for it's version, loads strings from JSON file rather than strings.xml etc)

NOTE: This is not a full class so some parts are missing, but I think it's more than enough to get the idea how I solved all this :)

/**
 * Created by bojank on 7/28/2014.
 * Class that handles custom resources downloaded from server
 */
public class ResourceManager {

    // List of ninePatchImages in the application
    private static ArrayList<HashMap<String, NinePatchDrawable>> ninePatchHashMaps;
    private static ArrayList<HashMap<String, Drawable>> imagesHashMaps;

    private static ImageLoader imageLoader;

    // Context for methods
    public static Context ctx;

    // JSONObject with all strings
    private static JSONObject joString;

    // JSONObject with all styles
    private static JSONObject joStyles;

    // String with current active lang code
    private static String currentLanguage;

    private static String sdcardPath;

    // Private consturctor to prevent creating a class instance
    private ResourceManager() {
    }

    /**
     * Method that returns a translated string for given key
     *
     * @param key String
     * @return String
     */
    public static String getString(String module, String key) {
        String output = ""; //String.format("[%s - %s]", module, key);

        try {
            if (getStringsFile() != null && getStringsFile().getJSONObject(module).has(key))
                output = getStringsFile().getJSONObject(module).getString(key);
        } catch (Exception e) {

            // Force some default language if proper json file is missing for newly added language
            currentLanguage = "en-US";
            Helper.saveLocale(currentLanguage, ctx);
            Helper.logError("ErrorFetchingString", e);
        }

        return output;
    }

    /**
     * Method that returns JSONObject with string resources
     * @return JSONObject
     * @throws JSONException
     */
    public static JSONObject getStringsFile() throws JSONException {

        if (joString == null) {
            String stringFileName = getResourcesPath() + "languages/" + getCurrentLanguage() + "/values.json";
            String languageFile = Helper.readJsonFile(stringFileName);
            if (languageFile != null) {
                joString = new JSONObject(Helper.readJsonFile(stringFileName));
            } else {
                return null;
            }
        }

        return joString.getJSONObject("strings");
    }

    /**
     * Method that returns current language ("sr", "en"...)
     * @return String
     */
    public static String getCurrentLanguage() {

        if (currentLanguage == null)
            currentLanguage = Helper.getCurrentLanguage(ctx);

        return currentLanguage;
    }

    /**
     * Method that resets joString object and currentLanguage on language change
     */
    public static void resetLanguage() {
        joString = null;
        currentLanguage = null;
    }

    /**
     * Method that resets joStyles object on theme change
     */
    public static void resetStyle() {
        joStyles = null;
    }

    /**
     * Method that deletes a directory from filesystem
     * @param path File
     * @return boolean
     */
    public static boolean deleteDirectory(File path) {
        if( path.exists() ) {
            File[] files = path.listFiles();
            for(int i=0; i<files.length; i++) {
                if(files[i].isDirectory()) {
                    deleteDirectory(files[i]);
                }
                else {
                    files[i].delete();
                }
            }
        }
        return(path.delete());
    }

    /**
    * Method that get's the version of assets file
    * @param url String
    */
    public static String getAssetsVersion(String url) throws IOException {
        Helper.logInfo("REQUEST URL:", url);
        OkHttpClient client = new OkHttpClient();

        // set connection timeut to 5min
        client.setConnectTimeout(1, TimeUnit.MINUTES);

        Request request = new Request.Builder()
                .url(url)
                .build();

        Response response = client.newCall(request).execute();
        return response.body().string();
    }

    /**
     * Method that downloads assets file from server
     * @param url String
     * @return String
     * @throws IOException
     */
    public static String getAssetsFile(String url) throws IOException {

        Helper.logInfo("REQUEST URL:", url);
        OkHttpClient client = new OkHttpClient();

        // set connection timeut to 5min
        client.setConnectTimeout(1, TimeUnit.MINUTES);

        Request request = new Request.Builder()
                .url(url)
                .header("User-Agent", MyApplication.USER_AGENT)
                .build();

        Response response = client.newCall(request).execute();
        InputStream inputStreamFile = response.body().byteStream();

        try {

            // Output stream
            String outputFileName = Environment.getExternalStorageDirectory().toString() + "/assets.zip";

            File deleteFile = new File(outputFileName);
            deleteFile.delete();

            OutputStream output = new FileOutputStream(outputFileName);

            byte data[] = new byte[1024];

            int count;

            // writing data to file
            while ((count = inputStreamFile.read(data)) != -1)
                output.write(data, 0, count);

            // flushing output
            output.flush();

            // closing streams
            output.close();
            inputStreamFile.close();

            return outputFileName;

        } catch (Exception e) {
            Helper.logError("Download Resursa", e);
            return "ERROR";
        }
    }

    public static void setStyle(View v, String styleName) {

        try {
            if (styleName == null || styleName.equals("")) {
                if (v instanceof EditText)
                    processStyle(v, getStylesFile().getJSONObject("EditText"));
            } else
                processStyle(v, getStylesFile().getJSONObject(styleName));
        } catch (Exception e) {
            Helper.logError("Setting Styles", e);
        }

    }

    private static void setBackground(View v, Drawable d) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
            v.setBackgroundDrawable(d);
        } else {
            v.setBackground(d);
        }
    }

    public static JSONObject getStylesFile() throws JSONException {

        if (joStyles == null) {
            String stylesFileName = getResourcesPath() + "styles/properties.json";
            joStyles = new JSONObject(Helper.readJsonFile(stylesFileName));
        }

        return joStyles;

    }

    public static void processStyle(View v, JSONObject joStyle) {

        if(joStyle != null) {
            try {

                // used for layout margins
                LinearLayout.LayoutParams layoutParams = null;

                if (Helper.isValidParameter(joStyle, "backgroundColor"))
                    v.setBackgroundColor(Color.parseColor(joStyle.getString("backgroundColor")));

                if (Helper.isValidParameter(joStyle, "backgroundImage"))
                    setBackground(v, loadNinePatchFromFilesystem(getImagesPath() + joStyle.getString("backgroundImage")));

                if (v instanceof TextView) {

                   applyTextViewParameters(v, joStyle);

                } else if (v instanceof  ListView) {

                    if (Helper.isValidParameter(joStyle, "dividerColor")) {
                        ((ListView) v).setDivider(new ColorDrawable(Color.parseColor(joStyle.getString("dividerColor"))));
                        ((ListView) v).setDividerHeight(Helper.convertDpToPixel(1));
                    }
                    if (Helper.isValidParameter(joStyle, "dividerHeight")) {
                        ((ListView) v).setDividerHeight(Helper.convertDpToPixel(joStyle.getInt("dividerHeight")));
                    }
                } else if (v instanceof UnderlinePageIndicator) {
                    if (Helper.isValidParameter(joStyle, "backgroundColor")) {
                        v.setBackgroundColor(Color.parseColor(joStyle.getString("backgroundColor")));
                    }
                    if (Helper.isValidParameter(joStyle, "selectedColor")) {
                        ((UnderlinePageIndicator) v).setSelectedColor(Color.parseColor(joStyle.getString("selectedColor")));
                    }
                } else if (v instanceof StyleableBackground) {
                    if (Helper.isValidParameter(joStyle, "backgroundColor")) {
                        View background = v.findViewById(R.id.llBackground);
                        if (background != null) {
                            background.setBackgroundColor(Color.parseColor(joStyle.getString("backgroundColor")));
                        }
                    }
                    if (Helper.isValidParameter(joStyle, "borderTopColor")) {
                        View topBorder = v.findViewById(R.id.llTopBorder);
                        if (topBorder != null) {

                            topBorder.setBackgroundColor(Color.parseColor(joStyle.getString("borderTopColor")));

                            if (Helper.isValidParameter(joStyle, "borderTopHeight")) {
                                topBorder.setMinimumHeight(Helper.convertDpToPixel(joStyle.getInt("borderTopHeight")));
                            }
                        }
                    }
                    if (Helper.isValidParameter(joStyle, "borderBottomColor")) {
                        View bottomBorder = v.findViewById(R.id.llBottomBorder);
                        if (bottomBorder != null) {

                            bottomBorder.setBackgroundColor(Color.parseColor(joStyle.getString("borderBottomColor")));

                            if (Helper.isValidParameter(joStyle, "borderBottomHeight")) {
                                bottomBorder.setMinimumHeight(Helper.convertDpToPixel(joStyle.getInt("borderBottomHeight")));
                            }
                        }
                    }
                    if (Helper.isValidParameter(joStyle, "backgroundImage")) {

                        ImageView ivBackgroundImage = (ImageView) v.findViewById(R.id.ivBackgroundImage);
                        if (ivBackgroundImage != null) {
                            BitmapDrawable d = (BitmapDrawable) ResourceManager.loadImageFromFilesystem(ResourceManager.getImagesPath() + joStyle.getString("backgroundImage"));

                            d.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
                            d.setGravity(Gravity.FILL_HORIZONTAL | Gravity.FILL_VERTICAL);
                            setBackground(ivBackgroundImage, d);
                        }
                    }
                }

                if(Helper.isValidParameter(joStyle, "width"))
                    v.setMinimumWidth(joStyle.getInt("width"));

                if(Helper.isValidParameter(joStyle, "height"))
                    v.setMinimumHeight(joStyle.getInt("height"));

                if(Helper.isValidParameter(joStyle, "padding"))
                    v.setPadding(joStyle.getInt("padding"), joStyle.getInt("padding"), joStyle.getInt("padding"), joStyle.getInt("padding"));

                if(Helper.isValidParameter(joStyle, "paddingLeft"))
                    v.setPadding(joStyle.getInt("paddingLeft"), v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom());

                if(Helper.isValidParameter(joStyle, "paddingTop"))
                    v.setPadding(v.getPaddingLeft(), joStyle.getInt("paddingTop"), v.getPaddingRight(), v.getPaddingBottom());

                if(Helper.isValidParameter(joStyle, "paddingRight"))
                    v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), joStyle.getInt("paddingRight"), v.getPaddingBottom());

                if(Helper.isValidParameter(joStyle, "paddingBottom"))
                    v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), joStyle.getInt("paddingBottom"));

                if(Helper.isValidParameter(joStyle, "margin")) {
                    layoutParams = new LinearLayout.LayoutParams(v.getLayoutParams());
                    layoutParams.setMargins(joStyle.getInt("margin"), joStyle.getInt("margin"), joStyle.getInt("margin"), joStyle.getInt("margin"));
                }

                if(Helper.isValidParameter(joStyle, "marginLeft")) {
                    layoutParams = new LinearLayout.LayoutParams(v.getLayoutParams());
                    layoutParams.setMargins(joStyle.getInt("marginLeft"), layoutParams.topMargin, layoutParams.rightMargin, layoutParams.bottomMargin);
                }

                if(Helper.isValidParameter(joStyle, "marginTop")) {
                    layoutParams = new LinearLayout.LayoutParams(v.getLayoutParams());
                    layoutParams.setMargins(layoutParams.leftMargin, joStyle.getInt("marginTop"), layoutParams.rightMargin, layoutParams.bottomMargin);
                }

                if(Helper.isValidParameter(joStyle, "marginRight")) {
                    layoutParams = new LinearLayout.LayoutParams(v.getLayoutParams());
                    layoutParams.setMargins(layoutParams.leftMargin, layoutParams.topMargin, joStyle.getInt("marginRight"), layoutParams.bottomMargin);
                }

                if(layoutParams != null)
                    v.setLayoutParams(layoutParams);


                RelativeLayout.LayoutParams relativeLayoutParams = null;

                if (Helper.isValidParameter(joStyle, "alignParentTop") && joStyle.getBoolean("alignParentTop")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
                }

                if (Helper.isValidParameter(joStyle, "alignParentLeft") && joStyle.getBoolean("alignParentLeft")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
                }

                if (Helper.isValidParameter(joStyle, "alignParentBottom") && joStyle.getBoolean("alignParentBottom")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
                }

                if (Helper.isValidParameter(joStyle, "alignParentRight") && joStyle.getBoolean("alignParentRight")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
                }

                if(Helper.isValidParameter(joStyle, "marginLeft")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.setMargins(joStyle.getInt("marginLeft"), relativeLayoutParams.topMargin, relativeLayoutParams.rightMargin, relativeLayoutParams.bottomMargin);
                }

                if(Helper.isValidParameter(joStyle, "marginTop")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.setMargins(relativeLayoutParams.leftMargin, joStyle.getInt("marginTop"), relativeLayoutParams.rightMargin, relativeLayoutParams.bottomMargin);
                }

                if(Helper.isValidParameter(joStyle, "marginRight")) {
                    relativeLayoutParams = new RelativeLayout.LayoutParams(v.getLayoutParams());
                    relativeLayoutParams.setMargins(relativeLayoutParams.leftMargin, relativeLayoutParams.topMargin, joStyle.getInt("marginRight"), relativeLayoutParams.bottomMargin);
                }

                if (relativeLayoutParams != null) {
                    v.setLayoutParams(relativeLayoutParams);
                }

            } catch (Exception e) {
                Helper.logError("", e);
            }
        }
    }

    public static String getSdcardPath() {

        if(sdcardPath == null)
            sdcardPath = ctx.getApplicationInfo().dataDir;

        return sdcardPath;
    }

    public static String getResourcesPath() {

        return getSdcardPath() + "/resources/";
    }

    public static String getCSSPath() {

        return getResourcesPath() + "default.css";
    }

    public static String getImagesPath() {

        return getResourcesPath() + "images/" + ResourceConstants.getScreenDPI(ctx) + "/";
    }

    public static String getImagesPathNoDpi() {

        return getResourcesPath() + "images/";
    }

    public static NinePatchDrawable loadNinePatchFromFilesystem(String filename) {

        if(ninePatchHashMaps == null)
            ninePatchHashMaps = new ArrayList<HashMap<String, NinePatchDrawable>>();

        // check if we already have this filename so we can reuse it
        for (int i = 0; i < ninePatchHashMaps.size(); i++) {
            HashMap<String, NinePatchDrawable> row = ninePatchHashMaps.get(i);

            if(row.containsKey(filename))
                return row.get(filename);
        }

        NinePatchDrawable patchy = null;
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
            Bitmap bitmap = BitmapFactory.decodeFile(filename, options);
            byte[] chunk = bitmap.getNinePatchChunk();
            boolean result = NinePatch.isNinePatchChunk(chunk);
            if (result)
                patchy = new NinePatchDrawable(bitmap, chunk, new Rect(), null);
        } catch (Exception e){
            Helper.logError("NinePatchLoading",e);
        }

        if(patchy != null) {

            HashMap<String, NinePatchDrawable> drawableImage = new HashMap<String, NinePatchDrawable>();
            drawableImage.put(filename, patchy);
            ninePatchHashMaps.add(drawableImage);
        }

        return patchy;

    }

    public static Drawable loadImageFromFilesystem(String filename) {

        if(imagesHashMaps == null)
            imagesHashMaps = new ArrayList<HashMap<String, Drawable>>();

        // check if we already have this filename so we can reuse it
        for (int i = 0; i < imagesHashMaps.size(); i++) {
            HashMap<String, Drawable> row = imagesHashMaps.get(i);

            if(row.containsKey(filename))
                return row.get(filename);
        }

        Drawable image = null;
        try {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.ARGB_8888;
            Bitmap bitmap = BitmapFactory.decodeFile(filename, options);

            if(bitmap == null)
                bitmap = BitmapFactory.decodeFile(filename.replace(ResourceConstants.getScreenDPI(ctx) + "/", ""), options);

            image = new BitmapDrawable(bitmap);
        } catch (Exception e){
            Helper.logError("ImageLoadingError",e);
        }

        if(image != null) {
            HashMap<String, Drawable> drawableImage = new HashMap<String, Drawable>();
            drawableImage.put(filename, image);
            imagesHashMaps.add(drawableImage);
        }

        return image;
    }

}
like image 129
Bojan Kopanja Avatar answered Oct 29 '22 06:10

Bojan Kopanja