M build.gradle => build.gradle +2 -0
@@ 63,6 63,7 @@ dependencies {
implementation 'androidx.exifinterface:exifinterface:1.3.5'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+ implementation 'androidx.preference:preference:1.1.1'
implementation 'com.google.android.material:material:1.7.0'
implementation "androidx.emoji2:emoji2:1.2.0"
@@ 95,6 96,7 @@ dependencies {
implementation 'io.michaelrocks:libphonenumber-android:8.12.49'
implementation 'io.github.nishkarsh:android-permissions:2.1.6'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:3.0.1'
implementation 'com.github.ipld:java-cid:v1.3.1'
implementation 'com.splitwise:tokenautocomplete:3.0.2'
implementation 'me.saket:better-link-movement-method:2.2.0'
A src/cheogram/java/com/cheogram/android/ColorResourcesLoaderCreator.java => src/cheogram/java/com/cheogram/android/ColorResourcesLoaderCreator.java +74 -0
@@ 0,0 1,74 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.cheogram.android;
+
+import android.content.Context;
+import android.content.res.loader.ResourcesLoader;
+import android.content.res.loader.ResourcesProvider;
+import android.os.Build.VERSION_CODES;
+import android.os.ParcelFileDescriptor;
+import android.system.Os;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+/** This class creates a Resources Table at runtime and helps replace color Resources on the fly. */
+public final class ColorResourcesLoaderCreator {
+
+ private ColorResourcesLoaderCreator() {}
+
+ private static final String TAG = ColorResourcesLoaderCreator.class.getSimpleName();
+
+ @Nullable
+ public static ResourcesLoader create(
+ @NonNull Context context, @NonNull Map<Integer, Integer> colorMapping) {
+ try {
+ byte[] contentBytes = ColorResourcesTableCreator.create(context, colorMapping);
+ Log.i(TAG, "Table created, length: " + contentBytes.length);
+ if (contentBytes.length == 0) {
+ return null;
+ }
+ FileDescriptor arscFile = null;
+ try {
+ arscFile = Os.memfd_create("temp.arsc", /* flags= */ 0);
+ // Note: This must not be closed through the OutputStream.
+ try (OutputStream pipeWriter = new FileOutputStream(arscFile)) {
+ pipeWriter.write(contentBytes);
+
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(arscFile)) {
+ ResourcesLoader colorsLoader = new ResourcesLoader();
+ colorsLoader.addProvider(
+ ResourcesProvider.loadFromTable(pfd, /* assetsProvider= */ null));
+ return colorsLoader;
+ }
+ }
+ } finally {
+ if (arscFile != null) {
+ Os.close(arscFile);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to create the ColorResourcesTableCreator.", e);
+ }
+ return null;
+ }
+}
A src/cheogram/java/com/cheogram/android/ColorResourcesTableCreator.java => src/cheogram/java/com/cheogram/android/ColorResourcesTableCreator.java +621 -0
@@ 0,0 1,621 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.cheogram.android;
+
+import android.content.Context;
+import android.util.Pair;
+import androidx.annotation.ColorInt;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * This class consists of definitions of resource data structures and helps creates a Color
+ * Resources Table on the fly. It is a Java replicate of the framework's code, see
+ * frameworks/base/include/ResourceTypes.h.
+ */
+final class ColorResourcesTableCreator {
+ private ColorResourcesTableCreator() {}
+
+ private static final short HEADER_TYPE_RES_TABLE = 0x0002;
+ private static final short HEADER_TYPE_STRING_POOL = 0x0001;
+ private static final short HEADER_TYPE_PACKAGE = 0x0200;
+ private static final short HEADER_TYPE_TYPE = 0x0201;
+ private static final short HEADER_TYPE_TYPE_SPEC = 0x0202;
+
+ private static final byte ANDROID_PACKAGE_ID = 0x01;
+ private static final byte APPLICATION_PACKAGE_ID = 0x7F;
+
+ private static final String RESOURCE_TYPE_NAME_COLOR = "color";
+
+ private static byte typeIdColor;
+
+ private static final PackageInfo ANDROID_PACKAGE_INFO =
+ new PackageInfo(ANDROID_PACKAGE_ID, "android");
+
+ private static final Comparator<ColorResource> COLOR_RESOURCE_COMPARATOR =
+ new Comparator<ColorResource>() {
+ @Override
+ public int compare(ColorResource res1, ColorResource res2) {
+ return res1.entryId - res2.entryId;
+ }
+ };
+
+ static byte[] create(Context context, Map<Integer, Integer> colorMapping) throws IOException {
+ if (colorMapping.entrySet().isEmpty()) {
+ throw new IllegalArgumentException("No color resources provided for harmonization.");
+ }
+ PackageInfo applicationPackageInfo =
+ new PackageInfo(APPLICATION_PACKAGE_ID, context.getPackageName());
+
+ Map<PackageInfo, List<ColorResource>> colorResourceMap = new HashMap<>();
+ ColorResource colorResource = null;
+ for (Map.Entry<Integer, Integer> entry : colorMapping.entrySet()) {
+ colorResource =
+ new ColorResource(
+ entry.getKey(),
+ context.getResources().getResourceName(entry.getKey()),
+ entry.getValue());
+ if (!context
+ .getResources()
+ .getResourceTypeName(entry.getKey())
+ .equals(RESOURCE_TYPE_NAME_COLOR)) {
+ throw new IllegalArgumentException(
+ "Non color resource found: name="
+ + colorResource.name
+ + ", typeId="
+ + Integer.toHexString(colorResource.typeId & 0xFF));
+ }
+ PackageInfo packageInfo;
+ if (colorResource.packageId == ANDROID_PACKAGE_ID) {
+ packageInfo = ANDROID_PACKAGE_INFO;
+ } else if (colorResource.packageId == APPLICATION_PACKAGE_ID) {
+ packageInfo = applicationPackageInfo;
+ } else {
+ throw new IllegalArgumentException(
+ "Not supported with unknown package id: " + colorResource.packageId);
+ }
+ if (!colorResourceMap.containsKey(packageInfo)) {
+ colorResourceMap.put(packageInfo, new ArrayList<ColorResource>());
+ }
+ colorResourceMap.get(packageInfo).add(colorResource);
+ }
+ // Resource Type Ids are assigned by aapt arbitrarily, for each new type the next available
+ // number is assigned and used. The type id will be the same for resources that are the same
+ // type.
+ typeIdColor = colorResource.typeId;
+ if (typeIdColor == 0) {
+ throw new IllegalArgumentException("No color resources found for harmonization.");
+ }
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ new ResTable(colorResourceMap).writeTo(outputStream);
+ return outputStream.toByteArray();
+ }
+
+ /**
+ * A Table chunk contains: a set of Packages, where a Package is a collection of Resources and a
+ * set of strings used by the Resources contained in those Packages.
+ *
+ * <p>The set of strings are contained in a StringPool chunk. Each Package is contained in a
+ * corresponding Package chunk. The StringPool chunk immediately follows the Table chunk header.
+ * The Package chunks follow the StringPool chunk.
+ */
+ private static class ResTable {
+ private static final short HEADER_SIZE = 0x000C;
+
+ private final ResChunkHeader header;
+ private final int packageCount;
+ private final StringPoolChunk stringPool;
+ private final List<PackageChunk> packageChunks = new ArrayList<>();
+
+ ResTable(Map<PackageInfo, List<ColorResource>> colorResourceMap) {
+ packageCount = colorResourceMap.size();
+ stringPool = new StringPoolChunk();
+ for (Entry<PackageInfo, List<ColorResource>> entry : colorResourceMap.entrySet()) {
+ List<ColorResource> colorResources = entry.getValue();
+ Collections.sort(colorResources, COLOR_RESOURCE_COMPARATOR);
+ packageChunks.add(new PackageChunk(entry.getKey(), colorResources));
+ }
+ header = new ResChunkHeader(HEADER_TYPE_RES_TABLE, HEADER_SIZE, getOverallSize());
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ header.writeTo(outputStream);
+ outputStream.write(intToByteArray(packageCount));
+ stringPool.writeTo(outputStream);
+ for (PackageChunk packageChunk : packageChunks) {
+ packageChunk.writeTo(outputStream);
+ }
+ }
+
+ private int getOverallSize() {
+ int packageChunkSize = 0;
+ for (PackageChunk packageChunk : packageChunks) {
+ packageChunkSize += packageChunk.getChunkSize();
+ }
+ return HEADER_SIZE + stringPool.getChunkSize() + packageChunkSize;
+ }
+ }
+
+ /** Header that appears at the front of every data chunk in a resource. */
+ private static class ResChunkHeader {
+ // Type identifier for this chunk. The meaning of this value depends
+ // on the containing chunk.
+ private final short type;
+ // Size of the chunk header (in bytes). Adding this value to
+ // the address of the chunk allows you to find its associated data
+ // (if any).
+ private final short headerSize;
+ // Total size of this chunk (in bytes). This is the chunkSize plus
+ // the size of any data associated with the chunk. Adding this value
+ // to the chunk allows you to completely skip its contents (including
+ // any child chunks). If this value is the same as chunkSize, there is
+ // no data associated with the chunk.
+ private final int chunkSize;
+
+ ResChunkHeader(short type, short headerSize, int chunkSize) {
+ this.type = type;
+ this.headerSize = headerSize;
+ this.chunkSize = chunkSize;
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ outputStream.write(shortToByteArray(type));
+ outputStream.write(shortToByteArray(headerSize));
+ outputStream.write(intToByteArray(chunkSize));
+ }
+ }
+
+ /**
+ * Immediately following the Table header is a StringPool chunk. It consists of StringPool chunk
+ * header and StringPool chunk body.
+ */
+ private static class StringPoolChunk {
+ private static final short HEADER_SIZE = 0x001C;
+ private static final int FLAG_UTF8 = 0x00000100;
+ private static final int STYLED_SPAN_LIST_END = 0xFFFFFFFF;
+
+ private final ResChunkHeader header;
+ private final int stringCount;
+ private final int styledSpanCount;
+ private final int stringsStart;
+ private final int styledSpansStart;
+ private final List<Integer> stringIndex = new ArrayList<>();
+ private final List<Integer> styledSpanIndex = new ArrayList<>();
+ private final List<byte[]> strings = new ArrayList<>();
+ private final List<List<StringStyledSpan>> styledSpans = new ArrayList<>();
+
+ private final boolean utf8Encode;
+ private final int stringsPaddingSize;
+ private final int chunkSize;
+
+ StringPoolChunk(String... rawStrings) {
+ this(false, rawStrings);
+ }
+
+ StringPoolChunk(boolean utf8, String... rawStrings) {
+ utf8Encode = utf8;
+ int stringOffset = 0;
+ for (String string : rawStrings) {
+ Pair<byte[], List<StringStyledSpan>> processedString = processString(string);
+ stringIndex.add(stringOffset);
+ stringOffset += processedString.first.length;
+ strings.add(processedString.first);
+ styledSpans.add(processedString.second);
+ }
+ int styledSpanOffset = 0;
+ for (List<StringStyledSpan> styledSpanList : styledSpans) {
+ for (StringStyledSpan styledSpan : styledSpanList) {
+ stringIndex.add(stringOffset);
+ stringOffset += styledSpan.styleString.length;
+ strings.add(styledSpan.styleString);
+ }
+ styledSpanIndex.add(styledSpanOffset);
+ // Each span occupies 3 int32, plus one end mark per chunk
+ styledSpanOffset += styledSpanList.size() * 12 + 4;
+ }
+
+ // All chunk size needs to be a multiple of 4
+ int stringOffsetResidue = stringOffset % 4;
+ stringsPaddingSize = stringOffsetResidue == 0 ? 0 : 4 - stringOffsetResidue;
+ stringCount = strings.size();
+ styledSpanCount = strings.size() - rawStrings.length;
+
+ boolean hasStyledSpans = strings.size() - rawStrings.length > 0;
+ if (!hasStyledSpans) {
+ // No styled spans, clear relevant data
+ styledSpanIndex.clear();
+ styledSpans.clear();
+ }
+
+ // Int32 per index
+ stringsStart =
+ HEADER_SIZE
+ + stringCount * 4 // String index
+ + styledSpanIndex.size() * 4; // Styled span index
+ int stringsSize = stringOffset + stringsPaddingSize;
+ styledSpansStart = hasStyledSpans ? stringsStart + stringsSize : 0;
+ chunkSize = stringsStart + stringsSize + (hasStyledSpans ? styledSpanOffset : 0);
+ header = new ResChunkHeader(HEADER_TYPE_STRING_POOL, HEADER_SIZE, chunkSize);
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ header.writeTo(outputStream);
+ outputStream.write(intToByteArray(stringCount));
+ outputStream.write(intToByteArray(styledSpanCount));
+ outputStream.write(intToByteArray(utf8Encode ? FLAG_UTF8 : 0));
+ outputStream.write(intToByteArray(stringsStart));
+ outputStream.write(intToByteArray(styledSpansStart));
+ for (Integer index : stringIndex) {
+ outputStream.write(intToByteArray(index));
+ }
+ for (Integer index : styledSpanIndex) {
+ outputStream.write(intToByteArray(index));
+ }
+ for (byte[] string : strings) {
+ outputStream.write(string);
+ }
+ if (stringsPaddingSize > 0) {
+ outputStream.write(new byte[stringsPaddingSize]);
+ }
+ for (List<StringStyledSpan> styledSpanList : styledSpans) {
+ for (StringStyledSpan styledSpan : styledSpanList) {
+ styledSpan.writeTo(outputStream);
+ }
+ outputStream.write(intToByteArray(STYLED_SPAN_LIST_END));
+ }
+ }
+
+ int getChunkSize() {
+ return chunkSize;
+ }
+
+ private Pair<byte[], List<StringStyledSpan>> processString(String rawString) {
+ // Ignore styled spans, won't be used in our scenario.
+ return new Pair<>(
+ utf8Encode ? stringToByteArrayUtf8(rawString) : stringToByteArray(rawString),
+ Collections.<StringStyledSpan>emptyList());
+ }
+ }
+
+ /** This structure defines a span of style information associated with a string in the pool. */
+ private static class StringStyledSpan {
+
+ private byte[] styleString;
+ private int nameReference;
+ private int firstCharacterIndex;
+ private int lastCharacterIndex;
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ outputStream.write(intToByteArray(nameReference));
+ outputStream.write(intToByteArray(firstCharacterIndex));
+ outputStream.write(intToByteArray(lastCharacterIndex));
+ }
+ }
+
+ /**
+ * A Package chunk contains a set of Resources and a set of strings associated with those
+ * Resources. The Resources are grouped by type. For each of set of Resources of a given type that
+ * the Package chunk contains there is a TypeSpec chunk and one or more Type chunks.
+ *
+ * <p>The strings are stored in two StringPool chunks: the typeStrings StringPool chunk which
+ * contains the names of the types of the Resources defined in the Package; the keyStrings
+ * StringPool chunk which contains the names (keys) of the Resources defined in the Package.
+ */
+ private static class PackageChunk {
+ private static final short HEADER_SIZE = 0x0120;
+ private static final int PACKAGE_NAME_MAX_LENGTH = 128;
+
+ private final ResChunkHeader header;
+ private final PackageInfo packageInfo;
+ private final StringPoolChunk typeStrings;
+ private final StringPoolChunk keyStrings;
+ private final TypeSpecChunk typeSpecChunk;
+
+ PackageChunk(PackageInfo packageInfo, List<ColorResource> colorResources) {
+ this.packageInfo = packageInfo;
+
+ // Placeholder String type, since only XML color resources will be replaced at runtime.
+ typeStrings = new StringPoolChunk(false, "?1", "?2", "?3", "?4", "?5", "color");
+ String[] keys = new String[colorResources.size()];
+ for (int i = 0; i < colorResources.size(); i++) {
+ keys[i] = colorResources.get(i).name;
+ }
+ keyStrings = new StringPoolChunk(true, keys);
+ typeSpecChunk = new TypeSpecChunk(colorResources);
+
+ header = new ResChunkHeader(HEADER_TYPE_PACKAGE, HEADER_SIZE, getChunkSize());
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ header.writeTo(outputStream);
+ outputStream.write(intToByteArray(packageInfo.id));
+ char[] packageName = packageInfo.name.toCharArray();
+ for (int i = 0; i < PACKAGE_NAME_MAX_LENGTH; i++) {
+ if (i < packageName.length) {
+ outputStream.write(charToByteArray(packageName[i]));
+ } else {
+ outputStream.write(charToByteArray((char) 0));
+ }
+ }
+ outputStream.write(intToByteArray(HEADER_SIZE)); // Type strings offset
+ outputStream.write(intToByteArray(0)); // Last public type
+ outputStream.write(
+ intToByteArray(HEADER_SIZE + typeStrings.getChunkSize())); // Key strings offset
+ outputStream.write(intToByteArray(0)); // Last public key
+ outputStream.write(intToByteArray(0)); // Note
+ typeStrings.writeTo(outputStream);
+ keyStrings.writeTo(outputStream);
+ typeSpecChunk.writeTo(outputStream);
+ }
+
+ int getChunkSize() {
+ return HEADER_SIZE
+ + typeStrings.getChunkSize()
+ + keyStrings.getChunkSize()
+ + typeSpecChunk.getChunkSizeWithTypeChunk();
+ }
+ }
+
+ /**
+ * A specification of the resources defined by a particular type.
+ *
+ * <p>There should be one of these chunks for each resource type.
+ *
+ * <p>This structure is followed by an array of integers providing the set of configuration change
+ * flags (ResTable_config::CONFIG_*) that have multiple resources for that configuration. In
+ * addition, the high bit is set if that resource has been made public.
+ */
+ private static class TypeSpecChunk {
+ private static final short HEADER_SIZE = 0x0010;
+ private static final int SPEC_PUBLIC = 0x40000000;
+
+ private final ResChunkHeader header;
+ private final int entryCount;
+ private final int[] entryFlags;
+ private final TypeChunk typeChunk;
+
+ TypeSpecChunk(List<ColorResource> colorResources) {
+ entryCount = colorResources.get(colorResources.size() - 1).entryId + 1;
+ Set<Short> validEntryIds = new HashSet<>();
+ for (ColorResource colorResource : colorResources) {
+ validEntryIds.add(colorResource.entryId);
+ }
+ entryFlags = new int[entryCount];
+ // All color resources in the table are marked as PUBLIC.
+ for (short entryId = 0; entryId < entryCount; entryId++) {
+ if (validEntryIds.contains(entryId)) {
+ entryFlags[entryId] = SPEC_PUBLIC;
+ }
+ }
+
+ header = new ResChunkHeader(HEADER_TYPE_TYPE_SPEC, HEADER_SIZE, getChunkSize());
+
+ typeChunk = new TypeChunk(colorResources, validEntryIds, entryCount);
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ header.writeTo(outputStream);
+ outputStream.write(new byte[] {typeIdColor, 0x00, 0x00, 0x00});
+ outputStream.write(intToByteArray(entryCount));
+ for (int entryFlag : entryFlags) {
+ outputStream.write(intToByteArray(entryFlag));
+ }
+ typeChunk.writeTo(outputStream);
+ }
+
+ int getChunkSizeWithTypeChunk() {
+ return getChunkSize() + typeChunk.getChunkSize();
+ }
+
+ private int getChunkSize() {
+ return HEADER_SIZE + entryCount * 4; // Int32 per entry flag
+ }
+ }
+
+ /**
+ * A collection of resource entries for a particular resource data type.
+ *
+ * <p>There may be multiple of these chunks for a particular resource type, supply different
+ * configuration variations for the resource values of that type.
+ */
+ private static class TypeChunk {
+ private static final int OFFSET_NO_ENTRY = 0xFFFFFFFF;
+
+ private static final short HEADER_SIZE = 0x0054;
+ private static final byte CONFIG_SIZE = 0x40;
+
+ private final ResChunkHeader header;
+ private final int entryCount;
+ private final byte[] config = new byte[CONFIG_SIZE];
+ private final int[] offsetTable;
+ private final ResEntry[] resEntries;
+
+ TypeChunk(List<ColorResource> colorResources, Set<Short> entryIds, int entryCount) {
+ this.entryCount = entryCount;
+ this.config[0] = CONFIG_SIZE;
+
+ this.resEntries = new ResEntry[colorResources.size()];
+
+ for (int index = 0; index < colorResources.size(); index++) {
+ ColorResource colorResource = colorResources.get(index);
+ this.resEntries[index] = new ResEntry(index, colorResource.value);
+ }
+
+ this.offsetTable = new int[entryCount];
+ int currentOffset = 0;
+ for (short entryId = 0; entryId < entryCount; entryId++) {
+ if (entryIds.contains(entryId)) {
+ this.offsetTable[entryId] = currentOffset;
+ currentOffset += ResEntry.SIZE;
+ } else {
+ this.offsetTable[entryId] = OFFSET_NO_ENTRY;
+ }
+ }
+
+ this.header = new ResChunkHeader(HEADER_TYPE_TYPE, HEADER_SIZE, getChunkSize());
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ header.writeTo(outputStream);
+ outputStream.write(new byte[] {typeIdColor, 0x00, 0x00, 0x00});
+ outputStream.write(intToByteArray(entryCount));
+ outputStream.write(intToByteArray(getEntryStart()));
+ outputStream.write(config);
+ for (int offset : offsetTable) {
+ outputStream.write(intToByteArray(offset));
+ }
+ for (ResEntry entry : resEntries) {
+ entry.writeTo(outputStream);
+ }
+ }
+
+ int getChunkSize() {
+ return getEntryStart() + resEntries.length * ResEntry.SIZE;
+ }
+
+ private int getEntryStart() {
+ return HEADER_SIZE + getOffsetTableSize();
+ }
+
+ private int getOffsetTableSize() {
+ return offsetTable.length * 4; // One int32 per entry
+ }
+ }
+
+ /**
+ * This is the beginning of information about an entry in the resource table. It holds the
+ * reference to the name of this entry, and is immediately followed by one of: A Res_value
+ * structure, if FLAG_COMPLEX is -not- set. An array of ResTable_map structures, if FLAG_COMPLEX
+ * is set. These supply a set of name/value mappings of data.
+ */
+ private static class ResEntry {
+ private static final short ENTRY_SIZE = 8;
+ private static final short FLAG_PUBLIC = 0x0002; // Always set to "Public"
+ private static final short VALUE_SIZE = 8;
+ private static final byte DATA_TYPE_AARRGGBB = 0x1C; // Type #aarrggbb
+
+ private static final int SIZE = ENTRY_SIZE + VALUE_SIZE;
+
+ private final int keyStringIndex;
+ private final int data;
+
+ ResEntry(int keyStringIndex, @ColorInt int data) {
+ this.keyStringIndex = keyStringIndex;
+ this.data = data;
+ }
+
+ void writeTo(ByteArrayOutputStream outputStream) throws IOException {
+ outputStream.write(shortToByteArray(ENTRY_SIZE));
+ outputStream.write(shortToByteArray(FLAG_PUBLIC));
+ outputStream.write(intToByteArray(keyStringIndex));
+ outputStream.write(shortToByteArray(VALUE_SIZE));
+ outputStream.write(new byte[] {0x00, DATA_TYPE_AARRGGBB});
+ outputStream.write(intToByteArray(data));
+ }
+ }
+
+ /** The basic info of a package, which consists of the id and the name of the package. */
+ static class PackageInfo {
+ private final int id;
+ private final String name;
+
+ PackageInfo(int id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+ }
+
+ /**
+ * A Color Resource object, which consists of the id of the package that the resource belongs to;
+ * the name and value of the color resource.
+ */
+ static class ColorResource {
+ private final byte packageId;
+ private final byte typeId;
+ private final short entryId;
+
+ private final String name;
+
+ @ColorInt private final int value;
+
+ ColorResource(int id, String name, int value) {
+ this.name = name;
+ this.value = value;
+
+ this.entryId = (short) (id & 0xFFFF);
+ this.typeId = (byte) ((id >> 16) & 0xFF);
+ this.packageId = (byte) ((id >> 24) & 0xFF);
+ }
+ }
+
+ private static byte[] shortToByteArray(short value) {
+ return new byte[] {
+ (byte) (value & 0xFF), (byte) ((value >> 8) & 0xFF),
+ };
+ }
+
+ private static byte[] charToByteArray(char value) {
+ return new byte[] {
+ (byte) (value & 0xFF), (byte) ((value >> 8) & 0xFF),
+ };
+ }
+
+ private static byte[] intToByteArray(int value) {
+ return new byte[] {
+ (byte) (value & 0xFF),
+ (byte) ((value >> 8) & 0xFF),
+ (byte) ((value >> 16) & 0xFF),
+ (byte) ((value >> 24) & 0xFF),
+ };
+ }
+
+ private static byte[] stringToByteArray(String value) {
+ char[] chars = value.toCharArray();
+ byte[] bytes = new byte[chars.length * 2 + 4];
+ byte[] lengthBytes = shortToByteArray((short) chars.length);
+ bytes[0] = lengthBytes[0];
+ bytes[1] = lengthBytes[1];
+ for (int i = 0; i < chars.length; i++) {
+ byte[] charBytes = charToByteArray(chars[i]);
+ bytes[i * 2 + 2] = charBytes[0];
+ bytes[i * 2 + 3] = charBytes[1];
+ }
+ bytes[bytes.length - 2] = 0;
+ bytes[bytes.length - 1] = 0; // EOS
+ return bytes;
+ }
+
+ private static byte[] stringToByteArrayUtf8(String value) {
+ byte[] rawBytes = value.getBytes(Charset.forName("UTF-8"));
+ byte stringLength = (byte) rawBytes.length;
+ byte[] bytes = new byte[rawBytes.length + 3];
+ System.arraycopy(rawBytes, 0, bytes, 2, stringLength);
+ bytes[0] = bytes[1] = stringLength;
+ bytes[bytes.length - 1] = 0; // EOS
+ return bytes;
+ }
+}
A src/cheogram/res/values-sw360dp/values-preference.xml => src/cheogram/res/values-sw360dp/values-preference.xml +3 -0
@@ 0,0 1,3 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <bool name="config_materialPreferenceIconSpaceReserved" tools:ignore="MissingDefaultResource,PrivateResource">false</bool>
+</resources>
M src/cheogram/res/values/colors.xml => src/cheogram/res/values/colors.xml +6 -0
@@ 2,4 2,10 @@
<resources>
<color name="splash_screen_background">#7401cf</color>
<color name="splash_screen_status_bar">#7401cf</color>
+ <color name="perpy">#7401CF</color>
+ <color name="yeller">#FFC700</color>
+
+ <color name="custom_theme_primary">@color/perpy</color>
+ <color name="custom_theme_primary_dark">#1E0036</color>
+ <color name="custom_theme_accent">#1E0036</color>
</resources>
M src/cheogram/res/values/strings.xml => src/cheogram/res/values/strings.xml +1 -0
@@ 24,5 24,6 @@
<string name="action_close">Close</string>
<string name="action_execute">Go</string>
<string name="pref_theme_oledblack">OLED Black</string>
+ <string name="pref_theme_custom">Custom</string>
<string name="invite_to_app">Invite to Chat</string>
</resources>
M src/cheogram/res/values/themes.xml => src/cheogram/res/values/themes.xml +60 -0
@@ 343,6 343,18 @@
<item name="unread_count">#1E0036</item>
</style>
+ <style name="ConversationsTheme.Custom" parent="ConversationsTheme">
+ <item name="colorPrimary">@color/custom_theme_primary</item>
+ <item name="colorPrimaryDark">@color/custom_theme_primary_dark</item>
+ <item name="colorAccent">@color/custom_theme_accent</item>
+ </style>
+
+ <style name="ConversationsTheme.CustomDark" parent="ConversationsTheme.Dark">
+ <item name="colorPrimary">@color/custom_theme_primary</item>
+ <item name="colorPrimaryDark">@color/custom_theme_primary_dark</item>
+ <item name="colorAccent">@color/custom_theme_accent</item>
+ </style>
+
<style name="ConversationsTheme.Medium" parent="ConversationsTheme">
<item name="TextSizeCaption">14sp</item>
<item name="TextSizeBody1">16sp</item>
@@ 379,6 391,30 @@
<item name="IconSize">20sp</item>
</style>
+ <style name="ConversationsTheme.CustomDark.Medium" parent="ConversationsTheme.CustomDark">
+ <item name="TextSizeCaption">14sp</item>
+ <item name="TextSizeBody1">16sp</item>
+ <item name="TextSizeBody2">16sp</item>
+ <item name="TextSizeSubhead">18sp</item>
+ <item name="TextSizeTitle">22sp</item>
+ <item name="TextSizeDisplay2">47sp</item>
+ <item name="TextSizeInput">18sp</item>
+ <item name="TextSeparation">6sp</item>
+ <item name="IconSize">20sp</item>
+ </style>
+
+ <style name="ConversationsTheme.Custom.Medium" parent="ConversationsTheme.Custom">
+ <item name="TextSizeCaption">14sp</item>
+ <item name="TextSizeBody1">16sp</item>
+ <item name="TextSizeBody2">16sp</item>
+ <item name="TextSizeSubhead">18sp</item>
+ <item name="TextSizeTitle">22sp</item>
+ <item name="TextSizeDisplay2">47sp</item>
+ <item name="TextSizeInput">18sp</item>
+ <item name="TextSeparation">6sp</item>
+ <item name="IconSize">20sp</item>
+ </style>
+
<style name="ConversationsTheme.OLEDBlack.Medium" parent="ConversationsTheme.OLEDBlack">
<item name="TextSizeCaption">14sp</item>
<item name="TextSizeBody1">16sp</item>
@@ 427,6 463,30 @@
<item name="IconSize">22sp</item>
</style>
+ <style name="ConversationsTheme.Custom.Large" parent="ConversationsTheme.Custom">
+ <item name="TextSizeCaption">16sp</item>
+ <item name="TextSizeBody1">18sp</item>
+ <item name="TextSizeBody2">18sp</item>
+ <item name="TextSizeSubhead">20sp</item>
+ <item name="TextSizeTitle">24sp</item>
+ <item name="TextSizeDisplay2">48sp</item>
+ <item name="TextSizeInput">20sp</item>
+ <item name="TextSeparation">7sp</item>
+ <item name="IconSize">22sp</item>
+ </style>
+
+ <style name="ConversationsTheme.CustomDark.Large" parent="ConversationsTheme.CustomDark">
+ <item name="TextSizeCaption">16sp</item>
+ <item name="TextSizeBody1">18sp</item>
+ <item name="TextSizeBody2">18sp</item>
+ <item name="TextSizeSubhead">20sp</item>
+ <item name="TextSizeTitle">24sp</item>
+ <item name="TextSizeDisplay2">48sp</item>
+ <item name="TextSizeInput">20sp</item>
+ <item name="TextSeparation">7sp</item>
+ <item name="IconSize">22sp</item>
+ </style>
+
<style name="ConversationsTheme.Large" parent="ConversationsTheme">
<item name="TextSizeCaption">16sp</item>
<item name="TextSizeBody1">18sp</item>
A src/cheogram/res/xml-v30/preferences.xml => src/cheogram/res/xml-v30/preferences.xml +370 -0
@@ 0,0 1,370 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.preference.PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:background="?attr/color_background_secondary"
+ android:key="main_screen">
+
+ <PreferenceCategory
+ android:key="general"
+ android:title="@string/pref_general">
+ <PreferenceScreen
+ android:key="huawei"
+ android:summary="@string/huawei_protected_apps_summary"
+ android:title="@string/huawei_protected_apps">
+ <intent
+ android:targetClass="com.huawei.systemmanager.optimize.process.ProtectActivity"
+ android:targetPackage="com.huawei.systemmanager" />
+ </PreferenceScreen>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_privacy"
+ android:key="privacy">
+ <CheckBoxPreference
+ android:defaultValue="@bool/confirm_messages"
+ android:key="confirm_messages"
+ android:summary="@string/pref_confirm_messages_summary"
+ android:title="@string/pref_confirm_messages" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/chat_states"
+ android:key="chat_states"
+ android:summary="@string/pref_chat_states_summary"
+ android:title="@string/pref_chat_states" />
+
+ <CheckBoxPreference
+ android:defaultValue="@bool/last_activity"
+ android:key="last_activity"
+ android:summary="@string/pref_broadcast_last_activity_summary"
+ android:title="@string/pref_broadcast_last_activity" />
+
+ <CheckBoxPreference
+ android:defaultValue="@bool/prevent_screenshots"
+ android:key="prevent_screenshots"
+ android:summary="@string/pref_prevent_screenshots_summary"
+ android:title="@string/pref_prevent_screenshots" />
+
+ <ListPreference
+ android:defaultValue="@string/omemo_setting_default"
+ android:entries="@array/omemo_setting_entries"
+ android:entryValues="@array/omemo_setting_entry_values"
+ android:key="omemo"
+ android:summary="@string/pref_omemo_setting_summary_default_on"
+ android:title="@string/pref_omemo_setting" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="notification_category"
+ android:title="@string/pref_notification_settings">
+ <CheckBoxPreference
+ android:defaultValue="@bool/notifications_from_strangers"
+ android:key="notifications_from_strangers"
+ android:summary="@string/pref_notifications_from_strangers_summary"
+ android:title="@string/pref_notifications_from_strangers" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/headsup_notifications"
+ android:key="notification_headsup"
+ android:summary="@string/pref_headsup_notifications_summary"
+ android:title="@string/pref_headsup_notifications" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/vibrate_on_notification"
+ android:key="vibrate_on_notification"
+ android:summary="@string/pref_vibrate_summary"
+ android:title="@string/pref_vibrate" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/led"
+ android:key="led"
+ android:summary="@string/pref_led_summary"
+ android:title="@string/pref_led" />
+ <ListPreference
+ android:defaultValue="@integer/grace_period"
+ android:entries="@array/grace_periods"
+ android:entryValues="@array/grace_periods_values"
+ android:key="grace_period_length"
+ android:summary="@string/pref_notification_grace_period_summary"
+ android:title="@string/pref_notification_grace_period" />
+ <PreferenceScreen
+ android:key="quiet_hours"
+ android:summary="@string/pref_quiet_hours_summary"
+ android:title="@string/title_pref_quiet_hours">
+ <CheckBoxPreference
+ android:defaultValue="@bool/enable_quiet_hours"
+ android:key="enable_quiet_hours"
+ android:summary="@string/pref_quiet_hours_summary"
+ android:title="@string/title_pref_enable_quiet_hours" />
+ <eu.siacs.conversations.ui.TimePreference
+ android:dependency="enable_quiet_hours"
+ android:key="quiet_hours_start"
+ android:negativeButtonText="@string/cancel"
+ android:positiveButtonText="@string/set"
+ android:title="@string/title_pref_quiet_hours_start_time" />
+ <eu.siacs.conversations.ui.TimePreference
+ android:dependency="enable_quiet_hours"
+ android:key="quiet_hours_end"
+ android:negativeButtonText="@string/cancel"
+ android:positiveButtonText="@string/set"
+ android:title="@string/title_pref_quiet_hours_end_time" />
+ <intent
+ android:action="android.intent.action.VIEW"
+ android:targetClass="eu.siacs.conversations.ui.SettingsActivity"
+ android:targetPackage="@string/applicationId">
+ <extra
+ android:name="page"
+ android:value="quiet_hours" />
+ </intent>
+ </PreferenceScreen>
+ <PreferenceScreen
+ android:key="message_notification_settings"
+ android:summary="@string/pref_more_notification_settings_summary"
+ android:title="@string/pref_message_notification_settings">
+ <intent android:action="android.settings.CHANNEL_NOTIFICATION_SETTINGS">
+ <extra
+ android:name="android.provider.extra.APP_PACKAGE"
+ android:value="@string/applicationId" />
+ <extra
+ android:name="android.provider.extra.CHANNEL_ID"
+ android:value="messages" />
+ </intent>
+ </PreferenceScreen>
+ <!--<RingtonePreference
+ android:defaultValue="@string/notification_ringtone"
+ android:key="notification_ringtone"
+ android:ringtoneType="notification"
+ android:summary="@string/pref_notification_sound_summary"
+ android:title="@string/pref_notification_sound" />
+ <RingtonePreference
+ android:defaultValue="@string/incoming_call_ringtone"
+ android:key="call_ringtone"
+ android:ringtoneType="ringtone"
+ android:summary="@string/pref_call_ringtone_summary"
+ android:title="@string/pref_ringtone" />-->
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="attachments"
+ android:title="@string/pref_attachments">
+ <CheckBoxPreference
+ android:defaultValue="@bool/use_share_location_plugin"
+ android:key="use_share_location_plugin"
+ android:summary="@string/pref_use_share_location_plugin_summary"
+ android:title="@string/pref_use_share_location_plugin" />
+ <ListPreference
+ android:defaultValue="@string/picture_compression"
+ android:entries="@array/picture_compression_entries"
+ android:entryValues="@array/picture_compression_values"
+ android:key="picture_compression"
+ android:summary="@string/pref_picture_compression_summary"
+ android:title="@string/pref_picture_compression" />
+ <ListPreference
+ android:defaultValue="@string/video_compression"
+ android:entries="@array/video_compression_entries"
+ android:entryValues="@array/video_compression_values"
+ android:key="video_compression"
+ android:summary="@string/pref_video_compression_summary"
+ android:title="@string/pref_video_compression" />
+ <ListPreference
+ android:defaultValue="@integer/auto_accept_filesize"
+ android:entries="@array/filesizes"
+ android:entryValues="@array/filesizes_values"
+ android:key="auto_accept_file_size"
+ android:summary="@string/pref_accept_files_summary"
+ android:title="@string/pref_accept_files" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_ui_options">
+ <CheckBoxPreference
+ android:defaultValue="@bool/use_green_background"
+ android:key="use_green_background"
+ android:summary="@string/pref_use_green_background_summary"
+ android:title="@string/pref_use_green_background" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/show_dynamic_tags"
+ android:key="show_dynamic_tags"
+ android:summary="@string/pref_show_dynamic_tags_summary"
+ android:title="@string/pref_show_dynamic_tags" />
+ <ListPreference
+ android:defaultValue="@string/theme"
+ android:entries="@array/themes"
+ android:entryValues="@array/themes_values"
+ android:key="theme"
+ android:summary="@string/pref_theme_options_summary"
+ android:title="@string/pref_theme_options" />
+ <com.rarepebble.colorpicker.ColorPreference
+ android:key="custom_theme_primary"
+ android:title="Custom Primary Color"
+ android:defaultValue="@color/custom_theme_primary" />
+ <ListPreference
+ android:defaultValue="@string/quick_action"
+ android:dialogTitle="@string/choose_quick_action"
+ android:entries="@array/quick_actions"
+ android:entryValues="@array/quick_action_values"
+ android:key="quick_action"
+ android:summary="@string/pref_quick_action_summary"
+ android:title="@string/pref_quick_action" />
+ <ListPreference
+ android:defaultValue="@string/default_font_size"
+ android:entries="@array/font_size_entries"
+ android:entryValues="@array/font_size_entry_values"
+ android:key="font_size"
+ android:summary="@string/pref_font_size_summary"
+ android:title="@string/pref_font_size" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="backup_category"
+ android:title="@string/backup">
+ <Preference
+ android:key="create_backup"
+ android:summary="@string/pref_create_backup_summary"
+ android:title="@string/pref_create_backup" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="advanced"
+ android:title="@string/pref_advanced_options">
+ <CheckBoxPreference
+ android:defaultValue="@bool/never_send"
+ android:key="never_send"
+ android:summary="@string/pref_never_send_crash_summary"
+ android:title="@string/pref_never_send_crash" />
+
+ <CheckBoxPreference
+ android:defaultValue="@bool/enable_foreground_service"
+ android:key="enable_foreground_service"
+ android:summary="@string/pref_keep_foreground_service_summary"
+ android:title="@string/pref_keep_foreground_service" />
+
+ <PreferenceScreen
+ android:key="expert"
+ android:summary="@string/pref_expert_options_summary"
+ android:title="@string/pref_expert_options">
+ <PreferenceCategory
+ android:key="security_options"
+ android:title="@string/pref_security_settings">
+ <CheckBoxPreference
+ android:defaultValue="@bool/btbv"
+ android:key="btbv"
+ android:summary="@string/pref_blind_trust_before_verification_summary"
+ android:title="@string/pref_blind_trust_before_verification" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/dont_trust_system_cas"
+ android:key="dont_trust_system_cas"
+ android:summary="@string/pref_dont_trust_system_cas_summary"
+ android:title="@string/pref_dont_trust_system_cas_title" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/validate_hostname"
+ android:key="validate_hostname"
+ android:summary="@string/pref_validate_hostname_summary"
+ android:title="@string/pref_validate_hostname" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/allow_message_correction"
+ android:key="allow_message_correction"
+ android:summary="@string/pref_allow_message_correction_summary"
+ android:title="@string/pref_allow_message_correction" />
+ <ListPreference
+ android:defaultValue="@integer/automatic_message_deletion"
+ android:key="automatic_message_deletion"
+ android:summary="@string/pref_automatically_delete_messages_description"
+ android:title="@string/pref_automatically_delete_messages" />
+ <Preference
+ android:key="remove_trusted_certificates"
+ android:summary="@string/pref_remove_trusted_certificates_summary"
+ android:title="@string/pref_remove_trusted_certificates_title" />
+ <Preference
+ android:key="clean_cache"
+ android:summary="@string/pref_clean_cache_summary"
+ android:title="@string/pref_clean_cache" />
+ <Preference
+ android:key="clean_private_storage"
+ android:summary="@string/pref_clean_private_storage_summary"
+ android:title="@string/pref_clean_private_storage" />
+ <Preference
+ android:key="delete_omemo_identities"
+ android:summary="@string/pref_delete_omemo_identities_summary"
+ android:title="@string/pref_delete_omemo_identities" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="connection_options"
+ android:title="@string/pref_connection_options">
+ <CheckBoxPreference
+ android:defaultValue="@bool/use_tor"
+ android:key="use_tor"
+ android:summary="@string/pref_use_tor_summary"
+ android:title="@string/pref_use_tor" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/show_connection_options"
+ android:key="show_connection_options"
+ android:summary="@string/pref_show_connection_options_summary"
+ android:title="@string/pref_show_connection_options" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_input_options">
+ <CheckBoxPreference
+ android:defaultValue="@bool/start_searching"
+ android:key="start_searching"
+ android:summary="@string/pref_start_search_summary"
+ android:title="@string/pref_start_search" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/enter_is_send"
+ android:key="enter_is_send"
+ android:summary="@string/pref_enter_is_send_summary"
+ android:title="@string/pref_enter_is_send" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/display_enter_key"
+ android:key="display_enter_key"
+ android:summary="@string/pref_display_enter_key_summary"
+ android:title="@string/pref_display_enter_key" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/scroll_to_bottom"
+ android:key="scroll_to_bottom"
+ android:summary="@string/pref_scroll_to_bottom_summary"
+ android:title="@string/pref_scroll_to_bottom" />
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/pref_presence_settings">
+ <CheckBoxPreference
+ android:defaultValue="@bool/manually_change_presence"
+ android:disableDependentsState="true"
+ android:key="manually_change_presence"
+ android:summary="@string/pref_manually_change_presence_summary"
+ android:title="@string/pref_manually_change_presence" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/away_when_screen_off"
+ android:dependency="manually_change_presence"
+ android:key="away_when_screen_off"
+ android:summary="@string/pref_away_when_screen_off_summary"
+ android:title="@string/pref_away_when_screen_off" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/dnd_on_silent_mode"
+ android:dependency="manually_change_presence"
+ android:key="dnd_on_silent_mode"
+ android:summary="@string/pref_dnd_on_silent_mode_summary"
+ android:title="@string/pref_dnd_on_silent_mode" />
+ <CheckBoxPreference
+ android:defaultValue="@bool/treat_vibrate_as_silent"
+ android:dependency="dnd_on_silent_mode"
+ android:key="treat_vibrate_as_silent"
+ android:summary="@string/pref_treat_vibrate_as_dnd_summary"
+ android:title="@string/pref_treat_vibrate_as_silent" />
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:key="group_chats"
+ android:title="@string/group_chats_and_channels">
+ <CheckBoxPreference
+ android:defaultValue="@bool/autojoin"
+ android:key="autojoin"
+ android:summary="@string/pref_autojoin_summary"
+ android:title="@string/pref_autojoin" />
+ <ListPreference
+ android:defaultValue="@string/default_channel_discovery"
+ android:entries="@array/channel_discovery_entries"
+ android:entryValues="@array/channel_discover_values"
+ android:key="channel_discovery_method"
+ android:summary="@string/pref_channel_discovery_summary"
+ android:title="@string/pref_channel_discovery" />
+ </PreferenceCategory>
+ <intent
+ android:action="android.intent.action.VIEW"
+ android:targetClass="eu.siacs.conversations.ui.SettingsActivity"
+ android:targetPackage="@string/applicationId">
+ <extra
+ android:name="page"
+ android:value="expert" />
+ </intent>
+ </PreferenceScreen>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/category_about">
+ <!-- <eu.siacs.conversations.ui.AboutPreference /> -->
+ </PreferenceCategory>
+</androidx.preference.PreferenceScreen>
M src/main/java/eu/siacs/conversations/ui/SettingsActivity.java => src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +14 -13
@@ 1,6 1,5 @@
package eu.siacs.conversations.ui;
-import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
@@ 9,19 8,20 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
-import android.preference.CheckBoxPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceManager;
-import android.preference.PreferenceScreen;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
+import androidx.fragment.app.FragmentManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
import com.google.common.base.Strings;
@@ 44,6 44,7 @@ import eu.siacs.conversations.ui.util.SettingsUtils;
import eu.siacs.conversations.ui.util.StyledAttributes;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.TimeFrameUtils;
+import eu.siacs.conversations.utils.ThemeHelper;
import eu.siacs.conversations.xmpp.Jid;
public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
@@ 69,7 70,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
- FragmentManager fm = getFragmentManager();
+ FragmentManager fm = getSupportFragmentManager();
mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content);
if (mSettingsFragment == null
|| !mSettingsFragment.getClass().equals(SettingsFragment.class)) {
@@ 79,6 80,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
mSettingsFragment.setActivityIntent(getIntent());
this.mTheme = findTheme();
setTheme(this.mTheme);
+ ThemeHelper.applyCustomColors(this);
getWindow()
.getDecorView()
.setBackgroundColor(
@@ 465,12 467,11 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
xmppConnectionService.reinitializeMuclumbusService();
} else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
xmppConnectionService.expireOldMessages(true);
- } else if (name.equals(THEME)) {
+ } else if (name.equals(THEME) || name.equals("custom_theme_primary")) {
final int theme = findTheme();
- if (this.mTheme != theme) {
- xmppConnectionService.setTheme(theme);
- recreate();
- }
+ xmppConnectionService.setTheme(theme);
+ ThemeHelper.applyCustomColors(xmppConnectionService);
+ recreate();
} else if (name.equals(PREVENT_SCREENSHOTS)) {
SettingsUtils.applyScreenshotPreventionSetting(this);
}
M src/main/java/eu/siacs/conversations/ui/SettingsFragment.java => src/main/java/eu/siacs/conversations/ui/SettingsFragment.java +17 -9
@@ 2,26 2,27 @@ package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceFragment;
-import android.preference.PreferenceScreen;
import android.text.TextUtils;
import android.widget.ListView;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+
+import com.rarepebble.colorpicker.ColorPreference;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.utils.Compatibility;
-public class SettingsFragment extends PreferenceFragment {
+public class SettingsFragment extends PreferenceFragmentCompat {
private String page = null;
@Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- addPreferencesFromResource(R.xml.preferences);
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.preferences, rootKey);
// Remove from standard preferences if the flag ONLY_INTERNAL_STORAGE is false
if (!Config.ONLY_INTERNAL_STORAGE) {
@@ 51,6 52,13 @@ public class SettingsFragment extends PreferenceFragment {
}
}
+ @Override
+ public void onDisplayPreferenceDialog(Preference preference) {
+ if (preference instanceof ColorPreference) {
+ ((ColorPreference) preference).showDialog(this, 0);
+ } else super.onDisplayPreferenceDialog(preference);
+ }
+
public void setActivityIntent(final Intent intent) {
boolean wasEmpty = TextUtils.isEmpty(page);
if (intent != null) {
M src/main/java/eu/siacs/conversations/ui/TimePreference.java => src/main/java/eu/siacs/conversations/ui/TimePreference.java +14 -11
@@ 2,11 2,14 @@ package eu.siacs.conversations.ui;
import android.content.Context;
import android.content.res.TypedArray;
-import android.preference.DialogPreference;
-import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TimePicker;
+import android.widget.LinearLayout;
+
+import androidx.preference.DialogPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
import java.text.DateFormat;
import java.util.Calendar;
@@ 36,30 39,30 @@ public class TimePreference extends DialogPreference implements Preference.OnPre
}
@Override
- protected View onCreateDialogView() {
+ public void onBindViewHolder(PreferenceViewHolder holder) {
picker = new TimePicker(getContext());
picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext()));
- return picker;
- }
- @SuppressWarnings("NullableProblems")
- @Override
- protected void onBindDialogView(final View v) {
- super.onBindDialogView(v);
long time = getPersistedLong(DEFAULT_VALUE);
picker.setCurrentHour((int) ((time % (24 * 60)) / 60));
picker.setCurrentMinute((int) ((time % (24 * 60)) % 60));
+
+ View view = holder.itemView;
+ LinearLayout widgetFrameView = ((LinearLayout)view.findViewById(android.R.id.widget_frame));
+ widgetFrameView.setVisibility(View.VISIBLE);
+ widgetFrameView.removeAllViews();
+ widgetFrameView.addView(picker);
}
- @Override
+ /*@Override
protected void onDialogClosed(final boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult) {
setTime(picker.getCurrentHour() * 60 + picker.getCurrentMinute());
}
- }
+ }*/
private static Calendar minutesToCalender(long time) {
final Calendar c = Calendar.getInstance();
M src/main/java/eu/siacs/conversations/ui/XmppActivity.java => src/main/java/eu/siacs/conversations/ui/XmppActivity.java +1 -0
@@ 417,6 417,7 @@ public abstract class XmppActivity extends ActionBarActivity {
this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
this.mTheme = findTheme();
setTheme(this.mTheme);
+ ThemeHelper.applyCustomColors(this);
}
protected boolean isCameraFeatureAvailable() {
M src/main/java/eu/siacs/conversations/utils/Compatibility.java => src/main/java/eu/siacs/conversations/utils/Compatibility.java +3 -3
@@ 10,13 10,13 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.os.Build;
-import android.preference.Preference;
-import android.preference.PreferenceCategory;
-import android.preference.PreferenceManager;
import android.util.Log;
import androidx.annotation.BoolRes;
import androidx.annotation.NonNull;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceManager;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
M src/main/java/eu/siacs/conversations/utils/ThemeHelper.java => src/main/java/eu/siacs/conversations/utils/ThemeHelper.java +20 -0
@@ 42,13 42,29 @@ import android.widget.TextView;
import androidx.annotation.StyleRes;
import androidx.core.content.ContextCompat;
+import com.cheogram.android.ColorResourcesLoaderCreator;
+
import com.google.android.material.snackbar.Snackbar;
+import java.util.HashMap;
+
import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.SettingsActivity;
public class ThemeHelper {
+ public static void applyCustomColors(final Context context) {
+ if (Build.VERSION.SDK_INT < 30) return;
+
+ final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ HashMap<Integer, Integer> colors = new HashMap<>();
+ if (sharedPreferences.contains("custom_theme_primary")) colors.put(R.color.custom_theme_primary, sharedPreferences.getInt("custom_theme_primary", 0));
+ if (colors.isEmpty()) return;
+
+ ResourceLoader loader = ColorResourcesLoaderCreator.create(context, colors);
+ if (loader != null) context.getResources().addLoaders(loader);
+ }
+
public static int find(final Context context) {
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
final Resources resources = context.getResources();
@@ 59,14 75,17 @@ public class ThemeHelper {
case "medium":
if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian_Medium;
else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack_Medium;
+ else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark_Medium : R.style.ConversationsTheme_Custom_Medium;
return dark ? R.style.ConversationsTheme_Dark_Medium : R.style.ConversationsTheme_Medium;
case "large":
if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian_Large;
else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack_Large;
+ else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark_Large : R.style.ConversationsTheme_Custom_Large;
return dark ? R.style.ConversationsTheme_Dark_Large : R.style.ConversationsTheme_Large;
default:
if ("obsidian".equals(setting)) return R.style.ConversationsTheme_Obsidian;
else if ("oledblack".equals(setting)) return R.style.ConversationsTheme_OLEDBlack;
+ else if ("custom".equals(setting)) return dark ? R.style.ConversationsTheme_CustomDark : R.style.ConversationsTheme_Custom;
return dark ? R.style.ConversationsTheme_Dark : R.style.ConversationsTheme;
}
}
@@ 91,6 110,7 @@ public class ThemeHelper {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && "automatic".equals(setting)) {
return (resources.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
} else {
+ if ("custom".equals(setting)) return sharedPreferences.getBoolean("custom_theme_dark", false);
return "dark".equals(setting) || "obsidian".equals(setting) || "oledblack".equals(setting);
}
}
M src/main/res/values/colors.xml => src/main/res/values/colors.xml +0 -2
@@ 1,7 1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
- <color name="perpy">#7401CF</color>
- <color name="yeller">#FFC700</color>
<color name="black">#ff000000</color>
<color name="black87">#de000000</color>
<color name="black54">#8a000000</color>