begin to implement any colour theme by Stephen Paul Weber
This commit is contained in:
parent
d989439e58
commit
af75e73802
15 changed files with 897 additions and 6 deletions
|
@ -82,6 +82,7 @@ dependencies {
|
|||
implementation 'com.googlecode.ez-vcard:ez-vcard:0.11.3'
|
||||
implementation 'org.jxmpp:jxmpp-jid:1.0.3'
|
||||
implementation 'org.hsluv:hsluv:0.2'
|
||||
implementation 'com.github.martin-stone:hsv-alpha-color-picker-android:2.4.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.25'
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
|
|
|
@ -94,3 +94,4 @@ public class PermissionsActivity extends AppCompatActivity
|
|||
void onPermissionGranted();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,4 +39,4 @@ public class StartUI extends AppCompatActivity {
|
|||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 de.monocles.chat;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
622
src/main/java/de/monocles/chat/ColorResourcesTableCreator.java
Normal file
622
src/main/java/de/monocles/chat/ColorResourcesTableCreator.java
Normal file
|
@ -0,0 +1,622 @@
|
|||
/*
|
||||
* 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 de.monocles.chat;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +70,8 @@ import net.java.otr4j.session.Session;
|
|||
import net.java.otr4j.session.SessionID;
|
||||
import net.java.otr4j.session.SessionImpl;
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
import eu.siacs.conversations.utils.ThemeHelper;
|
||||
import eu.siacs.conversations.xmpp.jid.OtrJidHelper;
|
||||
import eu.siacs.conversations.xmpp.Jid;
|
||||
|
||||
|
@ -1421,6 +1423,8 @@ public class XmppConnectionService extends Service {
|
|||
@Override
|
||||
public void onCreate() {
|
||||
updateNotificationChannels();
|
||||
setTheme(ThemeHelper.find(this));
|
||||
ThemeHelper.applyCustomColors(this);
|
||||
if (Compatibility.runsTwentySix()) {
|
||||
cleanOldNotificationChannels();
|
||||
}
|
||||
|
|
|
@ -111,6 +111,8 @@ import eu.siacs.conversations.xmpp.Jid;
|
|||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||
import me.drakeet.support.toast.ToastCompat;
|
||||
import eu.siacs.conversations.utils.ThemeHelper;
|
||||
|
||||
|
||||
public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoomDestroy {
|
||||
|
||||
|
@ -756,7 +758,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
|
|||
protected void onStart() {
|
||||
super.onStart();
|
||||
final int theme = findTheme();
|
||||
if (this.mTheme != theme) {
|
||||
if (this.mTheme != theme || !this.mCustomColors.equals(ThemeHelper.applyCustomColors(this))) {
|
||||
this.mSkipBackgroundBinding = true;
|
||||
recreate();
|
||||
} else {
|
||||
|
|
|
@ -53,6 +53,8 @@ import eu.siacs.conversations.utils.TimeFrameUtils;
|
|||
import eu.siacs.conversations.xmpp.Jid;
|
||||
import me.drakeet.support.toast.ToastCompat;
|
||||
import eu.siacs.conversations.services.UnifiedPushDistributor;
|
||||
import eu.siacs.conversations.utils.ThemeHelper;
|
||||
|
||||
|
||||
public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener {
|
||||
|
||||
|
@ -119,6 +121,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setTheme(ThemeHelper.find(this));
|
||||
ThemeHelper.applyCustomColors(this);
|
||||
setContentView(R.layout.activity_settings);
|
||||
FragmentManager fm = getFragmentManager();
|
||||
mSettingsFragment = (SettingsFragment) fm.findFragmentById(R.id.settings_content);
|
||||
|
@ -508,6 +511,13 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreference
|
|||
});
|
||||
updateTheme();
|
||||
}
|
||||
// TODO: handle skd<30
|
||||
// final String theTheme = PreferenceManager.getDefaultSharedPreferences(this).getString(THEME, "");
|
||||
// if (Build.VERSION.SDK_INT < 30 || !theTheme.equals("custom")) {
|
||||
// final PreferenceCategory uiCategory = (PreferenceCategory) mSettingsFragment.findPreference("UI");
|
||||
// final Preference customTheme = mSettingsFragment.findPreference("custom_theme");
|
||||
// if (customTheme != null) uiCategory.removePreference(customTheme);
|
||||
//}
|
||||
}
|
||||
|
||||
private void updateTheme() {
|
||||
|
@ -679,7 +689,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) || name.equals(THEME_COLOR)) {
|
||||
} else if (name.equals(THEME) || name.equals(THEME_COLOR) || name.equals("custom_theme_primary") || name.equals("custom_theme_primary_dark") || name.equals("custom_theme_accent") || name.equals("custom_theme_dark")) {
|
||||
final int theme = findTheme();
|
||||
xmppConnectionService.setTheme(theme);
|
||||
ThemeHelper.applyCustomColors(xmppConnectionService);
|
||||
recreate();
|
||||
updateTheme();
|
||||
} else if (name.equals(USE_UNICOLORED_CHATBG)) {
|
||||
xmppConnectionService.updateConversationUi();
|
||||
|
|
|
@ -65,6 +65,7 @@ import androidx.databinding.DataBindingUtil;
|
|||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Collections2;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
@ -140,6 +141,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
private boolean isCameraFeatureAvailable = false;
|
||||
|
||||
protected int mTheme;
|
||||
protected HashMap<Integer,Integer> mCustomColors;
|
||||
protected boolean mUsingEnterKey = false;
|
||||
public boolean mUseTor = false;
|
||||
public boolean mUseI2P = false;
|
||||
|
@ -479,6 +481,7 @@ public abstract class XmppActivity extends ActionBarActivity {
|
|||
super.onCreate(savedInstanceState);
|
||||
this.mTheme = findTheme();
|
||||
setTheme(this.mTheme);
|
||||
this.mCustomColors = ThemeHelper.applyCustomColors(this);
|
||||
metrics = getResources().getDisplayMetrics();
|
||||
ExceptionHelper.init(getApplicationContext());
|
||||
EmojiInitializationService.execute(this);
|
||||
|
|
|
@ -35,9 +35,13 @@ import android.content.res.ColorStateList;
|
|||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.TextView;
|
||||
import android.content.res.loader.ResourcesLoader;
|
||||
import de.monocles.chat.ColorResourcesLoaderCreator;
|
||||
import java.util.HashMap;
|
||||
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
@ -49,7 +53,20 @@ import eu.siacs.conversations.ui.SettingsActivity;
|
|||
import eu.siacs.conversations.ui.util.StyledAttributes;
|
||||
|
||||
public class ThemeHelper {
|
||||
public static HashMap<Integer, Integer> applyCustomColors(final Context context) {
|
||||
HashMap<Integer, Integer> colors = new HashMap<>();
|
||||
if (Build.VERSION.SDK_INT < 30) return colors;
|
||||
|
||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if (sharedPreferences.contains("custom_theme_primary")) colors.put(R.color.custom_theme_primary, sharedPreferences.getInt("custom_theme_primary", 0));
|
||||
if (sharedPreferences.contains("custom_theme_primary_dark")) colors.put(R.color.custom_theme_primary_dark, sharedPreferences.getInt("custom_theme_primary_dark", 0));
|
||||
if (sharedPreferences.contains("custom_theme_accent")) colors.put(R.color.custom_theme_accent, sharedPreferences.getInt("custom_theme_accent", 0));
|
||||
if (colors.isEmpty()) return colors;
|
||||
|
||||
ResourcesLoader loader = de.monocles.chat.ColorResourcesLoaderCreator.create(context, colors);
|
||||
if (loader != null) context.getResources().addLoaders(loader);
|
||||
return colors;
|
||||
}
|
||||
public static int find(final Context context) {
|
||||
final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final Resources resources = context.getResources();
|
||||
|
@ -227,6 +244,33 @@ public class ThemeHelper {
|
|||
return R.style.ConversationsTheme_Pink;
|
||||
}
|
||||
}
|
||||
case "custom":
|
||||
switch (fontSize) {
|
||||
case "medium":
|
||||
if (black) {
|
||||
return R.style.ConversationsTheme_CustomDark_Medium;
|
||||
} else if (dark) {
|
||||
return R.style.ConversationsTheme_CustomDark_Medium;
|
||||
} else {
|
||||
return R.style.ConversationsTheme_Custom_Medium;
|
||||
}
|
||||
case "large":
|
||||
if (black) {
|
||||
return R.style.ConversationsTheme_CustomDark_Large;
|
||||
} else if (dark) {
|
||||
return R.style.ConversationsTheme_CustomDark_Large;
|
||||
} else {
|
||||
return R.style.ConversationsTheme_Custom_Large;
|
||||
}
|
||||
default:
|
||||
if (black) {
|
||||
return R.style.ConversationsTheme_CustomDark;
|
||||
} else if (dark) {
|
||||
return R.style.ConversationsTheme_CustomDark;
|
||||
} else {
|
||||
return R.style.ConversationsTheme_Custom;
|
||||
}
|
||||
}
|
||||
default:
|
||||
if (black) {
|
||||
return R.style.ConversationsTheme_Monocles_Black;
|
||||
|
@ -439,7 +483,15 @@ public class ThemeHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDark(final SharedPreferences sharedPreferences, final Resources resources) {
|
||||
final String setting = sharedPreferences.getString(SettingsActivity.THEME, resources.getString(R.string.theme));
|
||||
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);
|
||||
}
|
||||
}
|
||||
public static boolean isDark(@StyleRes int id) {
|
||||
switch (id) {
|
||||
//blue
|
||||
|
@ -484,6 +536,10 @@ public class ThemeHelper {
|
|||
case R.style.ConversationsTheme_Pink_Black:
|
||||
case R.style.ConversationsTheme_Pink_Black_Large:
|
||||
case R.style.ConversationsTheme_Pink_Black_Medium:
|
||||
//custom
|
||||
case R.style.ConversationsTheme_CustomDark:
|
||||
case R.style.ConversationsTheme_CustomDark_Large:
|
||||
case R.style.ConversationsTheme_CustomDark_Medium:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
@ -514,6 +570,8 @@ public class ThemeHelper {
|
|||
return dark ? ContextCompat.getColorStateList(context, R.color.white70) : ContextCompat.getColorStateList(context, R.color.darkWhite);
|
||||
case "pink":
|
||||
return dark ? ContextCompat.getColorStateList(context, R.color.white70) : ContextCompat.getColorStateList(context, R.color.darkpink);
|
||||
case "custom":
|
||||
return dark ? ContextCompat.getColorStateList(context, R.color.white70) : ContextCompat.getColorStateList(context, R.color.darkWhite);
|
||||
default:
|
||||
return dark ? ContextCompat.getColorStateList(context, R.color.white70) : ContextCompat.getColorStateList(context, R.color.darkmonocles);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,8 @@
|
|||
<item>@string/pref_theme_orange</item>
|
||||
<item>@string/pref_theme_grey</item>
|
||||
<item>@string/pref_theme_pink</item>
|
||||
<!-- <item>@string/pref_theme_White</item> TODO: White Theme-->
|
||||
<item>@string/pref_theme_custom</item>
|
||||
<!-- <item>@string/pref_theme_White</item> TODO: White Theme-->
|
||||
</string-array>
|
||||
<string-array name="themecolor_values">
|
||||
<item>monocles</item>
|
||||
|
@ -26,7 +27,8 @@
|
|||
<item>orange</item>
|
||||
<item>grey</item>
|
||||
<item>pink</item>
|
||||
<!-- <item>White</item> TODO: White Theme-->
|
||||
<item>custom</item>
|
||||
<!-- <item>White</item> TODO: White Theme-->
|
||||
</string-array>
|
||||
|
||||
<string-array name="filesizes">
|
||||
|
|
|
@ -146,4 +146,13 @@
|
|||
|
||||
<!-- scanner -->
|
||||
<color name="scan_result_dots">#ff4caf50</color>
|
||||
|
||||
<!-- Cheogram -->
|
||||
<color name="perpy">#7401CF</color>
|
||||
<color name="black_perpy">#1E0036</color>
|
||||
<color name="yeller">#FFC700</color>
|
||||
|
||||
<color name="custom_theme_primary">@color/perpy</color>
|
||||
<color name="custom_theme_primary_dark">@color/black_perpy</color>
|
||||
<color name="custom_theme_accent">@color/black_perpy</color>
|
||||
</resources>
|
||||
|
|
|
@ -1251,4 +1251,5 @@
|
|||
<string name="pref_up_push_server_title">Push Server</string>
|
||||
<string name="pref_up_push_server_summary">A user-chosen push server to relay push messages via XMPP to your device.</string>
|
||||
<string name="no_account_deactivated">None (deactivated)</string>
|
||||
<string name="pref_theme_custom">Custom</string>
|
||||
</resources>
|
||||
|
|
|
@ -1538,5 +1538,64 @@
|
|||
<item name="TextSizeHeadline">22sp</item>
|
||||
</style>
|
||||
|
||||
<!-- Custom Theme -->
|
||||
<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.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.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>
|
||||
</resources>
|
|
@ -27,6 +27,46 @@
|
|||
android:key="theme_color"
|
||||
android:summary="@string/pref_theme_color_options_summary"
|
||||
android:title="@string/pref_theme_color_options" />
|
||||
<PreferenceScreen
|
||||
android:key="custom_theme"
|
||||
android:title="Custom Theme Options">
|
||||
<Preference
|
||||
android:key="pref_static_field_key"
|
||||
android:selectable="false"
|
||||
android:persistent="false"
|
||||
android:summary="You may sometimes have to force quit the app to get changes applied."/>
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="custom_theme_colors"
|
||||
android:title="Colors">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:defaultValue="false"
|
||||
android:key="custom_theme_dark"
|
||||
android:title="Custom Theme is Dark?" />
|
||||
<com.rarepebble.colorpicker.ColorPreference
|
||||
android:key="custom_theme_primary"
|
||||
android:title="Custom Primary Color"
|
||||
android:defaultValue="@color/perpy" />
|
||||
<com.rarepebble.colorpicker.ColorPreference
|
||||
android:key="custom_theme_primary_dark"
|
||||
android:title="Custom Primary Dark Color"
|
||||
android:defaultValue="@color/black_perpy" />
|
||||
<com.rarepebble.colorpicker.ColorPreference
|
||||
android:key="custom_theme_accent"
|
||||
android:title="Custom Accent Color"
|
||||
android:defaultValue="@color/black_perpy" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetClass="eu.siacs.conversations.ui.SettingsActivity"
|
||||
android:targetPackage="de.monocles.chat">
|
||||
<extra
|
||||
android:name="page"
|
||||
android:value="custom_theme" />
|
||||
</intent>
|
||||
</PreferenceScreen>
|
||||
<ListPreference
|
||||
android:defaultValue="@string/default_font_size"
|
||||
android:entries="@array/font_size_entries"
|
||||
|
|
Loading…
Reference in a new issue