patrickfav/under-the-hood

View on GitHub
hood-core/src/release/java/at/favre/lib/hood/internal/entries/KeyValueEntry.java

Summary

Maintainability
C
7 hrs
Test Coverage
package at.favre.lib.hood.internal.entries;

import android.app.Activity;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;

import java.util.AbstractMap;
import java.util.Comparator;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import at.favre.lib.hood.R;
import at.favre.lib.hood.interfaces.PageEntry;
import at.favre.lib.hood.interfaces.ViewTemplate;
import at.favre.lib.hood.interfaces.ViewTypes;
import at.favre.lib.hood.interfaces.actions.OnClickAction;
import at.favre.lib.hood.interfaces.values.DynamicValue;
import at.favre.lib.hood.util.PermissionTranslator;
import at.favre.lib.hood.util.defaults.DefaultMiscActions;
import at.favre.lib.hood.view.HoodDebugPageView;
import at.favre.lib.hood.view.KeyValueDetailDialogs;
import timber.log.Timber;

/**
 * An entry that has an key and value (e.g. normal properties). Supports custom click actions, multi line values
 * and dynamic values.
 */
public class KeyValueEntry implements Comparator<KeyValueEntry>, PageEntry<Map.Entry<CharSequence, KeyValueEntry.Value<String>>> {
    private static final ExecutorService THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(4, 4, 10, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>(1024));
    private static final Object monitor = new Object();

    private Map.Entry<CharSequence, Value<String>> data;
    private final boolean multiLine;
    private boolean loggingEnabled = true;

    /**
     * Creates Key-Value style page entry.
     *
     * @param key       as shown in ui
     * @param value     dynamic value (e.g. from {@link android.content.SharedPreferences}
     * @param action    used when clicked on
     * @param multiLine if a different layout should be used for long values
     */
    public KeyValueEntry(CharSequence key, DynamicValue<String> value, OnClickAction action, boolean multiLine) {
        this.data = new AbstractMap.SimpleEntry<>(key, new Value<>(value, value instanceof DynamicValue.Async, action));
        this.multiLine = multiLine;
    }

    /**
     * Creates Key-Value style page entry. Uses dialog as default click action.
     *
     * @param key       as shown in ui
     * @param value     dynamic value (e.g. from {@link android.content.SharedPreferences}
     * @param multiLine if a different layout should be used for long values
     */
    public KeyValueEntry(CharSequence key, DynamicValue<String> value, boolean multiLine) {
        this(key, value, new DialogClickAction(), multiLine);
    }

    /**
     * Creates Key-Value style page entry. Uses dialog as default click action and is not
     * multiline enabled.
     *
     * @param key   as shown in ui
     * @param value dynamic value (e.g. from {@link android.content.SharedPreferences}
     */
    public KeyValueEntry(CharSequence key, DynamicValue<String> value) {
        this(key, value, new DialogClickAction(), false);
    }

    /**
     * Creates Key-Value style page entry with a static value.
     *
     * @param key       as shown in ui
     * @param value     static value
     * @param action    used when clicked on
     * @param multiLine if a different layout should be used for long values
     */
    public KeyValueEntry(CharSequence key, final String value, OnClickAction action, boolean multiLine) {
        this(key, new DynamicValue.DefaultStaticValue<>(value), action, multiLine);
    }

    /**
     * Creates Key-Value style page entry with a static value. Uses dialog as default click action.
     *
     * @param key       as shown in ui
     * @param value     static value
     * @param multiLine if a different layout should be used for long values
     */
    public KeyValueEntry(CharSequence key, final String value, boolean multiLine) {
        this(key, new DynamicValue.DefaultStaticValue<>(value), new DialogClickAction(), multiLine);
    }

    /**
     * Creates Key-Value style page entry with a static value. Uses dialog as default click action and
     * ist not multi-line enabled.
     *
     * @param key   as shown in ui
     * @param value static value
     */
    public KeyValueEntry(CharSequence key, final String value) {
        this(key, value, false);
    }

    @Override
    public Map.Entry<CharSequence, KeyValueEntry.Value<String>> getValue() {
        return data;
    }

    @Override
    public ViewTemplate<Map.Entry<CharSequence, KeyValueEntry.Value<String>>> createViewTemplate() {
        return new Template(getViewType());
    }

    @Override
    public int getViewType() {
        return multiLine ? ViewTypes.VIEWTYPE_KEYVALUE_MULTILINE : ViewTypes.VIEWTYPE_KEYVALUE;
    }

    @Override
    public String toLogString() {
        return loggingEnabled ? "\t" + data.getKey() + "=" + data.getValue().getCachedValue() : null;
    }

    @Override
    public void disableLogging() {
        loggingEnabled = false;
    }

    @Override
    public void refresh() {
        data.getValue().setNeedsRefresh();
    }

    @Override
    public int compare(KeyValueEntry o1, KeyValueEntry o2) {
        return String.valueOf(o1.getValue().getKey()).compareTo(o2.getValue().getKey().toString());
    }

    private static class Template implements ViewTemplate<Map.Entry<CharSequence, KeyValueEntry.Value<String>>> {
        private ConcurrentHashMap<String, ValueBackgroundTask> taskMap = new ConcurrentHashMap<>();
        private final int viewType;

        Template(int viewType) {
            this.viewType = viewType;
        }

        @Override
        public int getViewType() {
            return viewType;
        }

        @Override
        public View constructView(ViewGroup viewGroup, LayoutInflater inflater) {
            if (viewType == ViewTypes.VIEWTYPE_KEYVALUE_MULTILINE) {
                return inflater.inflate(R.layout.hoodlib_template_keyvalue_multiline, viewGroup, false);
            } else {
                return inflater.inflate(R.layout.hoodlib_template_keyvalue, viewGroup, false);
            }
        }

        @Override
        public void setContent(final Map.Entry<CharSequence, KeyValueEntry.Value<String>> entry, @NonNull final View view) {
            ((TextView) view.findViewById(R.id.key)).setText(entry.getKey());
            TextView tvValue = view.findViewById(R.id.value);

            tvValue.setTag(entry.getValue().id);
            if (entry.getValue().needsRefresh && entry.getValue().processInBackground) {
                tvValue.setVisibility(View.GONE);
                view.findViewById(R.id.progress_bar).setVisibility(View.VISIBLE);
                view.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(view.getContext(), "Loading", Toast.LENGTH_SHORT).show();
                    }
                });
                view.setClickable(true);

                synchronized (monitor) {
                    ValueBackgroundTask task;
                    if (taskMap.containsKey(entry.getValue().id)) {
                        task = taskMap.get(entry.getValue().id);
                    } else {
                        Timber.d("starting task " + entry.getKey() + " id:" + entry.getValue().id);
                        task = new ValueBackgroundTask(entry.getValue());
                        taskMap.put(entry.getValue().id, task);
                        THREAD_POOL_EXECUTOR.execute(task);
                    }

                    task.setCallback(new Runnable() {
                        @Override
                        public void run() {
                            synchronized (monitor) {
                                taskMap.remove(entry.getValue().id);
                                setValueToView(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().getCachedValue()), view, entry.getValue().clickAction, entry.getValue().id);
                            }
                        }
                    });
                }
            } else {
                setValueToView(new AbstractMap.SimpleEntry<>(entry.getKey(), entry.getValue().getCachedValue()), view, entry.getValue().clickAction, entry.getValue().id);
            }
        }

        private void setValueToView(final Map.Entry<CharSequence, String> entry, @NonNull final View view, final OnClickAction onClickAction, final String tagId) {
            synchronized (monitor) {
                TextView tvValue = view.findViewById(R.id.value);
                if (tagId.equals(tvValue.getTag())) {
                    view.findViewById(R.id.progress_bar).setVisibility(View.GONE);
                    tvValue.setVisibility(View.VISIBLE);
                    tvValue.setText(entry.getValue());
                    if (onClickAction != null) {
                        view.setOnClickListener(new View.OnClickListener() {
                            @Override
                            public void onClick(View v) {
                                onClickAction.onClick(v, entry);
                            }
                        });
                        view.setClickable(true);
                    } else {
                        view.setOnClickListener(null);
                        view.setClickable(false);
                    }
                }
            }
        }

        @Override
        public void decorateViewWithZebra(@NonNull View view, @ColorInt int zebraColor, boolean isOdd) {
            HoodDebugPageView.setZebraToView(view, zebraColor, isOdd);
        }
    }

    /**
     * Task getting an async background value
     */
    static class ValueBackgroundTask implements Runnable, Comparable<ValueBackgroundTask> {
        private final long timestamp = SystemClock.elapsedRealtime();
        private Value<String> value;
        private Runnable callback;
        private Handler handler;

        ValueBackgroundTask(Value<String> value) {
            this.value = value;
            this.handler = new Handler(Looper.getMainLooper());
        }

        void setCallback(Runnable callback) {
            this.callback = callback;
        }

        @Override
        public void run() {
            value.setProcessedValue(value.dynamicValue.getValue());
            handler.post(callback);
        }

        @Override
        public int compareTo(@NonNull ValueBackgroundTask o) {
            return Long.valueOf(o.timestamp).compareTo(timestamp);
        }
    }

    /**
     * Wrapper for a value that can be refreshed in the background
     *
     * @param <T>
     */
    public static class Value<T> {
        final String id;
        T cache;
        final DynamicValue<T> dynamicValue;
        final boolean processInBackground;
        boolean needsRefresh;
        final OnClickAction clickAction;

        public Value(@NonNull DynamicValue<T> dynamicValue, boolean processInBackground, OnClickAction clickAction) {
            this.clickAction = clickAction;
            this.id = UUID.randomUUID().toString();
            this.processInBackground = processInBackground;
            this.dynamicValue = dynamicValue;
            this.needsRefresh = processInBackground;
            if (!processInBackground) {
                cache = dynamicValue.getValue();
            }
        }

        void setProcessedValue(T processedValue) {
            cache = processedValue;
            needsRefresh = false;
        }

        void setNeedsRefresh() {
            if (processInBackground) {
                cache = null;
                needsRefresh = true;
            } else {
                cache = dynamicValue.getValue();
                needsRefresh = false;
            }
        }

        T getCachedValue() {
            if (needsRefresh && !processInBackground) {
                cache = dynamicValue.getValue();
            }
            return cache;
        }

        @Override
        public String toString() {
            return cache != null ? cache.toString() : "null";
        }
    }

    /**
     * Use this to provide a additional long label used in e.g. default dialogs
     */
    public static class Label implements CharSequence, Comparable<Label> {
        public final CharSequence label;
        public final CharSequence longLabel;

        public Label(@NonNull CharSequence label) {
            this(label, label);
        }

        public Label(@NonNull CharSequence label, @NonNull CharSequence longLabel) {
            this.label = label;
            this.longLabel = longLabel;
        }

        @Override
        public int length() {
            return label.length();
        }

        @Override
        public char charAt(int i) {
            return label.charAt(i);
        }

        @Override
        public CharSequence subSequence(int i, int i1) {
            return label.subSequence(i, i1);
        }

        @NonNull
        @Override
        public String toString() {
            return label.toString();
        }

        @Override
        public int compareTo(@NonNull Label label) {
            return this.label.toString().compareTo(label.label.toString());
        }
    }

    private static CharSequence getLongLabel(CharSequence c) {
        if (c instanceof Label) {
            return ((Label) c).longLabel;
        }
        return c;
    }

    /* *************************************************************************** ONCLICKACTIONS */

    /**
     * Shows a simple toast with key/value
     */
    public static class ToastClickAction implements OnClickAction {
        @Override
        public void onClick(View v, Map.Entry<CharSequence, String> value) {
            Toast.makeText(v.getContext(), getLongLabel(value.getKey()) + "\n" + value.getValue(), Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * Starts the defined runtime permission check on click or shows current status if grandted
     */
    public static class AskPermissionClickAction implements OnClickAction {
        private final String androidPermissionName;
        private final Activity activity;

        public AskPermissionClickAction(String androidPermissionName, Activity activity) {
            this.androidPermissionName = androidPermissionName;
            this.activity = activity;
        }

        @Override
        public void onClick(View v, Map.Entry<CharSequence, String> value) {
            Timber.d("check android permissions for %s", androidPermissionName);
            @PermissionTranslator.PermissionState int permissionState = PermissionTranslator.getPermissionStatus(activity, androidPermissionName);

            if (permissionState == PermissionTranslator.GRANTED) {
                Toast.makeText(activity, R.string.hood_toast_already_allowed, Toast.LENGTH_SHORT).show();
                v.getContext().startActivity(DefaultMiscActions.getAppInfoIntent(v.getContext()));
            } else if (permissionState == PermissionTranslator.GRANTED_ON_INSTALL) {
                KeyValueDetailDialogs.DialogFragmentWrapper.newInstance(getLongLabel(value.getKey()), value.getValue())
                        .show(((Activity) v.getContext()).getFragmentManager(), String.valueOf(value.getKey()));
            } else {
                Timber.d("permission " + androidPermissionName + " not granted yet, show dialog");
                ActivityCompat.requestPermissions(activity, new String[]{androidPermissionName}, 2587);
            }
        }
    }

    /**
     * An click action that shows key/value in a dialog
     */
    public static class DialogClickAction implements OnClickAction {

        @Override
        public void onClick(View v, Map.Entry<CharSequence, String> value) {
            if (v.getContext() instanceof Activity) {
                KeyValueDetailDialogs.DialogFragmentWrapper.newInstance(getLongLabel(value.getKey()), value.getValue())
                        .show(((Activity) v.getContext()).getFragmentManager(), String.valueOf(value.getKey()));
            } else {
                new KeyValueDetailDialogs.CustomDialog(v.getContext(), getLongLabel(value.getKey()), value.getValue(), null).show();
            }
        }
    }

    /**
     * An click action that starts an {@link Intent}
     */
    public static class StartIntentAction implements OnClickAction {
        private final Intent intent;

        public StartIntentAction(Intent intent) {
            this.intent = intent;
        }

        @Override
        public void onClick(View v, Map.Entry<CharSequence, String> value) {
            v.getContext().startActivity(intent);
        }
    }
}