+ * This is a marker annotation and it has no specific attributes. + */ +@Documented +@Retention(CLASS) +@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE}) +public @interface NonNull { +} diff --git a/skin/skin-support/src/main/java/skin/support/annotation/Nullable.java b/skin/skin-support/src/main/java/skin/support/annotation/Nullable.java new file mode 100755 index 0000000000..0de1ae315c --- /dev/null +++ b/skin/skin-support/src/main/java/skin/support/annotation/Nullable.java @@ -0,0 +1,31 @@ +package skin.support.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.LOCAL_VARIABLE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PACKAGE; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Denotes that a parameter, field or method return value can be null. + *
+ * When decorating a method call parameter, this denotes that the parameter can + * legitimately be null and the method will gracefully deal with it. Typically + * used on optional parameters. + *
+ * When decorating a method, this denotes the method might legitimately return + * null. + *
+ * This is a marker annotation and it has no specific attributes.
+ */
+@Documented
+@Retention(CLASS)
+@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
+public @interface Nullable {
+}
diff --git a/skin/skin-support/src/main/java/skin/support/annotation/Skinable.java b/skin/skin-support/src/main/java/skin/support/annotation/Skinable.java
new file mode 100755
index 0000000000..f387b20888
--- /dev/null
+++ b/skin/skin-support/src/main/java/skin/support/annotation/Skinable.java
@@ -0,0 +1,12 @@
+package skin.support.annotation;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Retention(RUNTIME)
+@Target({TYPE})
+public @interface Skinable {
+}
diff --git a/skin/skin-support/src/main/java/skin/support/annotation/StringRes.java b/skin/skin-support/src/main/java/skin/support/annotation/StringRes.java
new file mode 100755
index 0000000000..b6edf15e0b
--- /dev/null
+++ b/skin/skin-support/src/main/java/skin/support/annotation/StringRes.java
@@ -0,0 +1,21 @@
+package skin.support.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+/**
+ * Denotes that an integer parameter, field or method return value is expected
+ * to be a String resource reference (e.g. {@code android.R.string.ok}).
+ */
+@Documented
+@Retention(CLASS)
+@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
+public @interface StringRes {
+}
diff --git a/skin/skin-support/src/main/java/skin/support/app/SkinActivityLifecycle.java b/skin/skin-support/src/main/java/skin/support/app/SkinActivityLifecycle.java
new file mode 100755
index 0000000000..779a87faea
--- /dev/null
+++ b/skin/skin-support/src/main/java/skin/support/app/SkinActivityLifecycle.java
@@ -0,0 +1,197 @@
+package skin.support.app;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+
+import java.lang.ref.WeakReference;
+import java.util.WeakHashMap;
+
+import skin.support.SkinCompatManager;
+import skin.support.annotation.Skinable;
+import skin.support.content.res.SkinCompatResources;
+import skin.support.observe.SkinObservable;
+import skin.support.observe.SkinObserver;
+import skin.support.utils.Slog;
+import skin.support.view.LayoutInflaterCompat;
+import skin.support.widget.SkinCompatSupportable;
+import skin.support.content.res.SkinCompatThemeUtils;
+
+import static skin.support.widget.SkinCompatHelper.INVALID_ID;
+import static skin.support.widget.SkinCompatHelper.checkResourceId;
+
+public class SkinActivityLifecycle implements Application.ActivityLifecycleCallbacks {
+ private static final String TAG = "SkinActivityLifecycle";
+ private static volatile SkinActivityLifecycle sInstance = null;
+ private WeakHashMap Note: this is a very inefficient way to access the array contents, it
+ * requires generating a number of temporary objects. Note:
Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.
+ */ + @Override + public SetNote: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.
+ */ + @Override + public Collection+ * It's best-effort, but any time we can throw something more diagnostic than an + * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to + * save a lot of development time. + *
+ * Good times to look for CME include after any allocArrays() call and at the end of
+ * functions that change mSize (put/remove/clear).
+ */
+ private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
+
+ /**
+ * The minimum amount by which the capacity of a ArrayMap will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+ private static final int BASE_SIZE = 4;
+
+ /**
+ * Maximum number of entries to have in array caches.
+ */
+ private static final int CACHE_SIZE = 10;
+
+ /**
+ * Caches of small array objects to avoid spamming garbage. The cache
+ * Object[] variable is a pointer to a linked list of array objects.
+ * The first entry in the array is a pointer to the next array in the
+ * list; the second entry is a pointer to the int[] hash code array for it.
+ */
+ static Object[] mBaseCache;
+ static int mBaseCacheSize;
+ static Object[] mTwiceBaseCache;
+ static int mTwiceBaseCacheSize;
+
+ int[] mHashes;
+ Object[] mArray;
+ int mSize;
+
+ private static int binarySearchHashes(int[] hashes, int N, int hash) {
+ try {
+ return ContainerHelpers.binarySearch(hashes, N, hash);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
+ throw new ConcurrentModificationException();
+ } else {
+ throw e; // the cache is poisoned at this point, there's not much we can do
+ }
+ }
+ }
+
+ int indexOf(Object key, int hash) {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, hash);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (key.equals(mArray[index << 1])) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == hash; end++) {
+ if (key.equals(mArray[end << 1])) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
+ if (key.equals(mArray[i << 1])) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ int indexOfNull() {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, 0);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (null == mArray[index << 1]) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == 0; end++) {
+ if (null == mArray[end << 1]) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
+ if (null == mArray[i << 1]) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ @SuppressWarnings("ArrayToString")
+ private void allocArrays(final int size) {
+ if (size == (BASE_SIZE * 2)) {
+ synchronized (ArrayMap.class) {
+ if (mTwiceBaseCache != null) {
+ final Object[] array = mTwiceBaseCache;
+ mArray = array;
+ mTwiceBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ array[0] = array[1] = null;
+ mTwiceBaseCacheSize--;
+ if (DEBUG) System.out.println(TAG + " Retrieving 2x cache " + mHashes
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ return;
+ }
+ }
+ } else if (size == BASE_SIZE) {
+ synchronized (ArrayMap.class) {
+ if (mBaseCache != null) {
+ final Object[] array = mBaseCache;
+ mArray = array;
+ mBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ array[0] = array[1] = null;
+ mBaseCacheSize--;
+ if (DEBUG) System.out.println(TAG + " Retrieving 1x cache " + mHashes
+ + " now have " + mBaseCacheSize + " entries");
+ return;
+ }
+ }
+ }
+
+ mHashes = new int[size];
+ mArray = new Object[size << 1];
+ }
+
+ @SuppressWarnings("ArrayToString")
+ private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
+ if (hashes.length == (BASE_SIZE * 2)) {
+ synchronized (ArrayMap.class) {
+ if (mTwiceBaseCacheSize < CACHE_SIZE) {
+ array[0] = mTwiceBaseCache;
+ array[1] = hashes;
+ for (int i = (size << 1) - 1; i >= 2; i--) {
+ array[i] = null;
+ }
+ mTwiceBaseCache = array;
+ mTwiceBaseCacheSize++;
+ if (DEBUG) System.out.println(TAG + " Storing 2x cache " + array
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ }
+ }
+ } else if (hashes.length == BASE_SIZE) {
+ synchronized (ArrayMap.class) {
+ if (mBaseCacheSize < CACHE_SIZE) {
+ array[0] = mBaseCache;
+ array[1] = hashes;
+ for (int i = (size << 1) - 1; i >= 2; i--) {
+ array[i] = null;
+ }
+ mBaseCache = array;
+ mBaseCacheSize++;
+ if (DEBUG) System.out.println(TAG + " Storing 1x cache " + array
+ + " now have " + mBaseCacheSize + " entries");
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new empty ArrayMap. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+ public SimpleArrayMap() {
+ mHashes = ContainerHelpers.EMPTY_INTS;
+ mArray = ContainerHelpers.EMPTY_OBJECTS;
+ mSize = 0;
+ }
+
+ /**
+ * Create a new ArrayMap with a given initial capacity.
+ */
+ @SuppressWarnings("NullAway") // allocArrays initializes mHashes and mArray.
+ public SimpleArrayMap(int capacity) {
+ if (capacity == 0) {
+ mHashes = ContainerHelpers.EMPTY_INTS;
+ mArray = ContainerHelpers.EMPTY_OBJECTS;
+ } else {
+ allocArrays(capacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Create a new ArrayMap with the mappings from the given ArrayMap.
+ */
+ public SimpleArrayMap(SimpleArrayMap This implementation returns false if the object is not a Map or
+ * SimpleArrayMap, or if the maps have different sizes. Otherwise, for each
+ * key in this map, values of both maps are compared. If the values for any
+ * key are not equal, the method returns false, otherwise it returns true.
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof SimpleArrayMap) {
+ SimpleArrayMap, ?> map = (SimpleArrayMap, ?>) object;
+ if (size() != map.size()) {
+ return false;
+ }
+
+ try {
+ for (int i = 0; i < mSize; i++) {
+ K key = keyAt(i);
+ V mine = valueAt(i);
+ Object theirs = map.get(key);
+ if (mine == null) {
+ if (theirs != null || !map.containsKey(key)) {
+ return false;
+ }
+ } else if (!mine.equals(theirs)) {
+ return false;
+ }
+ }
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ return true;
+ } else if (object instanceof Map) {
+ Map, ?> map = (Map, ?>) object;
+ if (size() != map.size()) {
+ return false;
+ }
+
+ try {
+ for (int i = 0; i < mSize; i++) {
+ K key = keyAt(i);
+ V mine = valueAt(i);
+ Object theirs = map.get(key);
+ if (mine == null) {
+ if (theirs != null || !map.containsKey(key)) {
+ return false;
+ }
+ } else if (!mine.equals(theirs)) {
+ return false;
+ }
+ }
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int[] hashes = mHashes;
+ final Object[] array = mArray;
+ int result = 0;
+ for (int i = 0, v = 1, s = mSize; i < s; i++, v += 2) {
+ Object value = array[v];
+ result += hashes[i] ^ (value == null ? 0 : value.hashCode());
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a key or a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i = 0; i < mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ Object key = keyAt(i);
+ if (key != this) {
+ buffer.append(key);
+ } else {
+ buffer.append("(this Map)");
+ }
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/skin/skin-support/src/main/java/skin/support/content/res/ColorState.java b/skin/skin-support/src/main/java/skin/support/content/res/ColorState.java
new file mode 100755
index 0000000000..380851589d
--- /dev/null
+++ b/skin/skin-support/src/main/java/skin/support/content/res/ColorState.java
@@ -0,0 +1,600 @@
+package skin.support.content.res;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.text.TextUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import skin.support.annotation.ColorRes;
+import skin.support.exception.SkinCompatException;
+import skin.support.utils.Slog;
+
+public final class ColorState {
+ private static final String TAG = "ColorState";
+ boolean onlyDefaultColor;
+ String colorName;
+ String colorWindowFocused;
+ String colorSelected;
+ String colorFocused;
+ String colorEnabled;
+ String colorPressed;
+ String colorChecked;
+ String colorActivated;
+ String colorAccelerated;
+ String colorHovered;
+ String colorDragCanAccept;
+ String colorDragHovered;
+ String colorDefault;
+
+ ColorState(String colorWindowFocused, String colorSelected, String colorFocused,
+ String colorEnabled, String colorPressed, String colorChecked, String colorActivated,
+ String colorAccelerated, String colorHovered, String colorDragCanAccept,
+ String colorDragHovered, String colorDefault) {
+ this.colorWindowFocused = colorWindowFocused;
+ this.colorSelected = colorSelected;
+ this.colorFocused = colorFocused;
+ this.colorEnabled = colorEnabled;
+ this.colorPressed = colorPressed;
+ this.colorChecked = colorChecked;
+ this.colorActivated = colorActivated;
+ this.colorAccelerated = colorAccelerated;
+ this.colorHovered = colorHovered;
+ this.colorDragCanAccept = colorDragCanAccept;
+ this.colorDragHovered = colorDragHovered;
+ this.colorDefault = colorDefault;
+ this.onlyDefaultColor = TextUtils.isEmpty(colorWindowFocused)
+ && TextUtils.isEmpty(colorSelected)
+ && TextUtils.isEmpty(colorFocused)
+ && TextUtils.isEmpty(colorEnabled)
+ && TextUtils.isEmpty(colorPressed)
+ && TextUtils.isEmpty(colorChecked)
+ && TextUtils.isEmpty(colorActivated)
+ && TextUtils.isEmpty(colorAccelerated)
+ && TextUtils.isEmpty(colorHovered)
+ && TextUtils.isEmpty(colorDragCanAccept)
+ && TextUtils.isEmpty(colorDragHovered);
+ if (onlyDefaultColor) {
+ if (!colorDefault.startsWith("#")) {
+ throw new SkinCompatException("Default color cannot be a reference, when only default color is available!");
+ }
+ }
+ }
+
+ ColorState(String colorName, String colorDefault) {
+ this.colorName = colorName;
+ this.colorDefault = colorDefault;
+ this.onlyDefaultColor = true;
+ if (!colorDefault.startsWith("#")) {
+ throw new SkinCompatException("Default color cannot be a reference, when only default color is available!");
+ }
+ }
+
+ public boolean isOnlyDefaultColor() {
+ return onlyDefaultColor;
+ }
+
+ public String getColorName() {
+ return colorName;
+ }
+
+ public String getColorWindowFocused() {
+ return colorWindowFocused;
+ }
+
+ public String getColorSelected() {
+ return colorSelected;
+ }
+
+ public String getColorFocused() {
+ return colorFocused;
+ }
+
+ public String getColorEnabled() {
+ return colorEnabled;
+ }
+
+ public String getColorPressed() {
+ return colorPressed;
+ }
+
+ public String getColorChecked() {
+ return colorChecked;
+ }
+
+ public String getColorActivated() {
+ return colorActivated;
+ }
+
+ public String getColorAccelerated() {
+ return colorAccelerated;
+ }
+
+ public String getColorHovered() {
+ return colorHovered;
+ }
+
+ public String getColorDragCanAccept() {
+ return colorDragCanAccept;
+ }
+
+ public String getColorDragHovered() {
+ return colorDragHovered;
+ }
+
+ public String getColorDefault() {
+ return colorDefault;
+ }
+
+ ColorStateList parse() {
+ if (onlyDefaultColor) {
+ int defaultColor = Color.parseColor(colorDefault);
+ return ColorStateList.valueOf(defaultColor);
+ }
+ return parseAll();
+ }
+
+ private ColorStateList parseAll() {
+ int stateColorCount = 0;
+ List