diff --git a/build.gradle b/build.gradle index d782ab6b8..0825bf01b 100644 --- a/build.gradle +++ b/build.gradle @@ -89,6 +89,7 @@ dependencies { implementation 'com.github.AppIntro:AppIntro:6.1.0' implementation 'androidx.browser:browser:1.3.0' // 1.4.0 needs minCompileSdk 31 implementation 'com.otaliastudios:transcoder:0.9.1' // 0.10.4 seems to be buggy + implementation project(':libs:AXML') implementation fileTree(include: ['libwebrtc-m92.aar'], dir: 'libs') } diff --git a/libs/AXML/.gitignore b/libs/AXML/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/libs/AXML/.gitignore @@ -0,0 +1 @@ +/build diff --git a/libs/AXML/build.gradle b/libs/AXML/build.gradle new file mode 100644 index 000000000..e2a71a2c2 --- /dev/null +++ b/libs/AXML/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'java' + +group = 'fr.xgouchet' + + +dependencies { + // TODO UNIT TESTS +} + +// Additional tasks for jitpack.io + +// build a jar with source files +task sourcesJar(type: Jar) { + from sourceSets.main.java.srcDirs + archiveClassifier.set("sources") +} + +// build a jar with javadoc +task javadocJar(type: Jar, dependsOn: javadoc) { + archiveClassifier.set("javadoc") + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} \ No newline at end of file diff --git a/libs/AXML/proguard-rules.pro b/libs/AXML/proguard-rules.pro new file mode 100644 index 000000000..1ecf581f6 --- /dev/null +++ b/libs/AXML/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /opt/android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/libs/AXML/src/main/java/fr/xgouchet/axml/Attribute.java b/libs/AXML/src/main/java/fr/xgouchet/axml/Attribute.java new file mode 100644 index 000000000..67fdf2b09 --- /dev/null +++ b/libs/AXML/src/main/java/fr/xgouchet/axml/Attribute.java @@ -0,0 +1,66 @@ +package fr.xgouchet.axml; + +public class Attribute { + + /** + * @return the name + */ + public String getName() { + return mName; + } + + /** + * @return the prefix + */ + public String getPrefix() { + return mPrefix; + } + + /** + * @return the namespace + */ + public String getNamespace() { + return mNamespace; + } + + /** + * @return the value + */ + public String getValue() { + return mValue; + } + + /** + * @param name + * the name to set + */ + public void setName(final String name) { + mName = name; + } + + /** + * @param prefix + * the prefix to set + */ + public void setPrefix(final String prefix) { + mPrefix = prefix; + } + + /** + * @param namespace + * the namespace to set + */ + public void setNamespace(final String namespace) { + mNamespace = namespace; + } + + /** + * @param value + * the value to set + */ + public void setValue(final String value) { + mValue = value; + } + + private String mName, mPrefix, mNamespace, mValue; +} \ No newline at end of file diff --git a/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlDomListener.java b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlDomListener.java new file mode 100644 index 000000000..da2584f1e --- /dev/null +++ b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlDomListener.java @@ -0,0 +1,92 @@ +package fr.xgouchet.axml; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import java.util.Stack; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +public class CompressedXmlDomListener implements CompressedXmlParserListener { + + /** + * @throws ParserConfigurationException if a DocumentBuilder can't be created + */ + public CompressedXmlDomListener() throws ParserConfigurationException { + mBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + mStack = new Stack<>(); + } + + public void startDocument() { + mDocument = mBuilder.newDocument(); + mStack.push(mDocument); + } + + public void endDocument() { + } + + public void startPrefixMapping(String prefix, String uri) { + } + + public void endPrefixMapping(String prefix, String uri) { + } + + public void startElement(final String uri, final String localName, + final String qName, final Attribute[] attrs) { + Element elt; + + // create elt + if (isEmpty(uri)) { + elt = mDocument.createElement(localName); + } else { + elt = mDocument.createElementNS(uri, qName); + } + + // add attrs + for (Attribute attr : attrs) { + if (isEmpty(attr.getNamespace())) { + elt.setAttribute(attr.getName(), attr.getValue()); + } else { + elt.setAttributeNS(attr.getNamespace(), attr.getPrefix() + ':' + + attr.getName(), attr.getValue()); + } + } + + // handle stack + mStack.peek().appendChild(elt); + mStack.push(elt); + } + + public void endElement(String uri, String localName, String qName) { + mStack.pop(); + } + + public void characterData(String data) { + mStack.peek().appendChild(mDocument.createCDATASection(data)); + } + + public void text(String data) { + mStack.peek().appendChild(mDocument.createTextNode(data)); + } + + public void processingInstruction(String target, String data) { + } + + /** + * @return the parsed document + */ + public Document getDocument() { + return mDocument; + } + + private static boolean isEmpty(String text) { + return (text == null) || "".equals(text); + } + + private Stack mStack; + private Document mDocument; + private final DocumentBuilder mBuilder; +} diff --git a/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParser.java b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParser.java new file mode 100644 index 000000000..5e3c28959 --- /dev/null +++ b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParser.java @@ -0,0 +1,515 @@ +package fr.xgouchet.axml; + +import org.w3c.dom.Document; + +import java.io.IOException; +import java.io.InputStream; +import java.text.DecimalFormat; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.parsers.ParserConfigurationException; + + +public class CompressedXmlParser { + + public static final String TAG = "CXP"; + + public static final int WORD_START_DOCUMENT = 0x00080003; + + public static final int WORD_STRING_TABLE = 0x001C0001; + public static final int WORD_RES_TABLE = 0x00080180; + + public static final int WORD_START_NS = 0x00100100; + public static final int WORD_END_NS = 0x00100101; + public static final int WORD_START_TAG = 0x00100102; + public static final int WORD_END_TAG = 0x00100103; + public static final int WORD_TEXT = 0x00100104; + public static final int WORD_EOS = 0xFFFFFFFF; + public static final int WORD_SIZE = 4; + + private static final int TYPE_ID_REF = 0x01000008; + private static final int TYPE_ATTR_REF = 0x02000008; + private static final int TYPE_STRING = 0x03000008; + private static final int TYPE_DIMEN = 0x05000008; + private static final int TYPE_FRACTION = 0x06000008; + private static final int TYPE_INT = 0x10000008; + private static final int TYPE_FLOAT = 0x04000008; + + private static final int TYPE_FLAGS = 0x11000008; + private static final int TYPE_BOOL = 0x12000008; + private static final int TYPE_COLOR = 0x1C000008; + private static final int TYPE_COLOR2 = 0x1D000008; + + private static final String[] DIMEN = new String[]{"px", "dp", "sp", + "pt", "in", "mm"}; + + public CompressedXmlParser() { + mNamespaces = new HashMap<>(); + } + + /** + * Parses the xml data in the given file, + * + * @param input the source input to parse + * @param listener the listener for XML events (must not be null) + * @throws IOException if the input can't be read + */ + public void parse(final InputStream input, + final CompressedXmlParserListener listener) throws IOException { + + if (listener == null) { + throw new IllegalArgumentException( + "CompressedXmlParser Listener can' be null"); + } + mListener = listener; + + // TODO is.available may not be accurate !!! + mData = new byte[input.available()]; + input.read(mData); + input.close(); + + // parseCompressedHeader(); + parseCompressedXml(); + + } + + /** + * Parses the xml data in the given file, + * + * @param input the source file to parse + * @return the DOM document object + * @throws IOException if the input can't be read + * @throws ParserConfigurationException if a DocumentBuilder can't be created + */ + public Document parseDOM(final InputStream input) throws IOException, + ParserConfigurationException { + CompressedXmlDomListener dom = new CompressedXmlDomListener(); + + parse(input, dom); + + return dom.getDocument(); + } + + /** + * Each tag starts with a 32 bits word (different for start tag, end tag and + * end doc) + */ + private void parseCompressedXml() { + int word0; + + while (mParserOffset < mData.length) { + word0 = getLEWord(mParserOffset); + switch (word0) { + case WORD_START_DOCUMENT: + parseStartDocument(); + break; + case WORD_STRING_TABLE: + parseStringTable(); + break; + case WORD_RES_TABLE: + parseResourceTable(); + break; + case WORD_START_NS: + parseNamespace(true); + break; + case WORD_END_NS: + parseNamespace(false); + break; + case WORD_START_TAG: + parseStartTag(); + break; + case WORD_END_TAG: + parseEndTag(); + break; + case WORD_TEXT: + parseText(); + break; + case WORD_EOS: + mListener.endDocument(); + break; + default: + mParserOffset += WORD_SIZE; +// Log.w(TAG, "Unknown word 0x" + Integer.toHexString(word0) +// + " @" + mParserOffset); + break; + } + } + + mListener.endDocument(); + } + + /** + * A doc starts with the following 4bytes words : + * + */ + private void parseStartDocument() { + mListener.startDocument(); + mParserOffset += (2 * WORD_SIZE); + } + + /** + * the string table starts with the following 4bytes words : + * + */ + private void parseStringTable() { + + int chunk = getLEWord(mParserOffset + (1 * WORD_SIZE)); + mStringsCount = getLEWord(mParserOffset + (2 * WORD_SIZE)); + mStylesCount = getLEWord(mParserOffset + (3 * WORD_SIZE)); + int strOffset = mParserOffset + + getLEWord(mParserOffset + (5 * WORD_SIZE)); + int styleOffset = getLEWord(mParserOffset + (6 * WORD_SIZE)); + + mStringsTable = new String[mStringsCount]; + int offset; + for (int i = 0; i < mStringsCount; ++i) { + offset = strOffset + + getLEWord(mParserOffset + ((i + 7) * WORD_SIZE)); + mStringsTable[i] = getStringFromStringTable(offset); + } + + if (styleOffset > 0) { +// Log.w(TAG, "Unread styles"); + for (int i = 0; i < mStylesCount; ++i) { + // TODO read the styles ??? + } + } + + mParserOffset += chunk; + } + + /** + * the resource ids table starts with the following 4bytes words : + * + */ + private void parseResourceTable() { + int chunk = getLEWord(mParserOffset + (1 * WORD_SIZE)); + mResCount = (chunk / 4) - 2; + + mResourcesIds = new int[mResCount]; + for (int i = 0; i < mResCount; ++i) { + mResourcesIds[i] = getLEWord(mParserOffset + ((i + 2) * WORD_SIZE)); + } + + mParserOffset += chunk; + } + + /** + * A namespace tag contains the following 4bytes words : + * + */ + private void parseNamespace(boolean start) { + final int prefixIdx = getLEWord(mParserOffset + (4 * WORD_SIZE)); + final int uriIdx = getLEWord(mParserOffset + (5 * WORD_SIZE)); + + final String uri = getString(uriIdx); + final String prefix = getString(prefixIdx); + + if (start) { + mListener.startPrefixMapping(prefix, uri); + mNamespaces.put(uri, prefix); + } else { + mListener.endPrefixMapping(prefix, uri); + mNamespaces.remove(uri); + } + + // Offset to first tag + mParserOffset += (6 * WORD_SIZE); + } + + /** + * A start tag will start with the following 4bytes words : + * + */ + private void parseStartTag() { + // get tag info + final int uriIdx = getLEWord(mParserOffset + (4 * WORD_SIZE)); + final int nameIdx = getLEWord(mParserOffset + (5 * WORD_SIZE)); + final int attrCount = getLEShort(mParserOffset + (7 * WORD_SIZE)); + + final String name = getString(nameIdx); + String uri, qname; + if (uriIdx == 0xFFFFFFFF) { + uri = ""; + qname = name; + } else { + uri = getString(uriIdx); + if (mNamespaces.containsKey(uri)) { + qname = mNamespaces.get(uri) + ':' + name; + } else { + qname = name; + } + } + + // offset to start of attributes + mParserOffset += (9 * WORD_SIZE); + + final Attribute[] attrs = new Attribute[attrCount]; // NOPMD + for (int a = 0; a < attrCount; a++) { + attrs[a] = parseAttribute(); // NOPMD + + // offset to next attribute or tag + mParserOffset += (5 * 4); + } + + mListener.startElement(uri, name, qname, attrs); + } + + /** + * An attribute will have the following 4bytes words : + * + */ + private Attribute parseAttribute() { + final int attrNSIdx = getLEWord(mParserOffset); + final int attrNameIdx = getLEWord(mParserOffset + (1 * WORD_SIZE)); + final int attrValueIdx = getLEWord(mParserOffset + (2 * WORD_SIZE)); + final int attrType = getLEWord(mParserOffset + (3 * WORD_SIZE)); + final int attrData = getLEWord(mParserOffset + (4 * WORD_SIZE)); + + final Attribute attr = new Attribute(); + attr.setName(getString(attrNameIdx)); + + if (attrNSIdx == 0xFFFFFFFF) { + attr.setNamespace(null); + attr.setPrefix(null); + } else { + String uri = getString(attrNSIdx); + if (mNamespaces.containsKey(uri)) { + attr.setNamespace(uri); + attr.setPrefix(mNamespaces.get(uri)); + } + } + + if (attrValueIdx == 0xFFFFFFFF) { + attr.setValue(getAttributeValue(attrType, attrData)); + } else { + attr.setValue(getString(attrValueIdx)); + } + + return attr; + + } + + /** + * A text will start with the following 4bytes word : + * + */ + private void parseText() { + // get tag infos + final int strIndex = getLEWord(mParserOffset + (4 * WORD_SIZE)); + + String data = getString(strIndex); + mListener.characterData(data); + + // offset to next node + mParserOffset += (7 * WORD_SIZE); + } + + /** + * EndTag contains the following 4bytes words : + * + */ + private void parseEndTag() { + // get tag info + final int uriIdx = getLEWord(mParserOffset + (4 * WORD_SIZE)); + final int nameIdx = getLEWord(mParserOffset + (5 * WORD_SIZE)); + + final String name = getString(nameIdx); + String uri; + if (uriIdx == 0xFFFFFFFF) { + uri = ""; + } else { + uri = getString(uriIdx); + } + + mListener.endElement(uri, name, null); + + // offset to start of next tag + mParserOffset += (6 * WORD_SIZE); + } + + /** + * @param index the index of the string in the StringIndexTable + * @return the string + */ + private String getString(final int index) { + String res; + if ((index >= 0) && (index < mStringsCount)) { + res = mStringsTable[index]; + } else { + res = null; // NOPMD + } + + return res; + } + + /** + * @param offset offset of the beginning of the string inside the StringTable + * (and not the whole data array) + * @return the String + */ + private String getStringFromStringTable(final int offset) { + int strLength; + byte chars[]; + if (mData[offset + 1] == mData[offset]) { + strLength = mData[offset]; + chars = new byte[strLength];// NOPMD + for (int i = 0; i < strLength; i++) { + chars[i] = mData[offset + 2 + i]; // NOPMD + } + } else { + + strLength = ((mData[offset + 1] << 8) & 0xFF00) + | (mData[offset] & 0xFF); + chars = new byte[strLength]; // NOPMD + for (int i = 0; i < strLength; i++) { + chars[i] = mData[offset + 2 + (i * 2)]; // NOPMD + } + + } + return new String(chars); + } + + /** + * @param off the offset of the word to read + * @return value of a Little Endian 32 bit word from the byte arrayat offset + * off. + */ + private int getLEWord(final int off) { + return ((mData[off + 3] << 24) & 0xff000000) + | ((mData[off + 2] << 16) & 0x00ff0000) + | ((mData[off + 1] << 8) & 0x0000ff00) + | ((mData[off + 0] << 0) & 0x000000ff); + } + + /** + * @param off the offset of the word to read + * @return value of a Little Endian 16 bit word from the byte array at offset + * off. + */ + private int getLEShort(final int off) { + return ((mData[off + 1] << 8) & 0xff00) + | ((mData[off + 0] << 0) & 0x00ff); + } + + /** + * @param type the attribute type + * @param data the data value + * @return the typed value + */ + private String getAttributeValue(final int type, final int data) { + String res; + + switch (type) { + case TYPE_STRING: + res = getString(data); + break; + case TYPE_DIMEN: + res = Integer.toString(data >> 8) + DIMEN[data & 0xFF]; + break; + case TYPE_FRACTION: + double fracValue = (((double) data) / ((double) 0x7FFFFFFF)); + // res = String.format("%.2f%%", fracValue); + res = new DecimalFormat("#.##%").format(fracValue); + break; + case TYPE_FLOAT: + res = Float.toString(Float.intBitsToFloat(data)); + break; + case TYPE_INT: + case TYPE_FLAGS: + res = Integer.toString(data); + break; + case TYPE_BOOL: + res = Boolean.toString(data != 0); + break; + case TYPE_COLOR: + case TYPE_COLOR2: + res = String.format("#%08X", data); + break; + case TYPE_ID_REF: + res = String.format("@id/0x%08X", data); + break; + case TYPE_ATTR_REF: + res = String.format("?id/0x%08X", data); + break; + default: +// Log.w(TAG, "(type=" + Integer.toHexString(type) + ") : " + data +// + " (0x" + Integer.toHexString(data) + ") @" +// + mParserOffset); + res = String.format("%08X/0x%08X", type, data); + break; + } + + return res; + } + + // Data + private CompressedXmlParserListener mListener; + + // Internal + private Map mNamespaces; + private byte[] mData; + + private String[] mStringsTable; + private int[] mResourcesIds; + private int mStringsCount, mStylesCount, mResCount; + private int mParserOffset; + +} diff --git a/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParserListener.java b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParserListener.java new file mode 100644 index 000000000..2738713d6 --- /dev/null +++ b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlParserListener.java @@ -0,0 +1,101 @@ +package fr.xgouchet.axml; + +public interface CompressedXmlParserListener { + + /** + * Receive notification of the beginning of a document. + */ + void startDocument(); + + /** + * Receive notification of the end of a document. + */ + void endDocument(); + + /** + * Begin the scope of a prefix-URI Namespace mapping. + * + * @param prefix + * the Namespace prefix being declared. An empty string is used + * for the default element namespace, which has no prefix. + * @param uri + * the Namespace URI the prefix is mapped to + */ + void startPrefixMapping(String prefix, String uri); + + /** + * End the scope of a prefix-URI mapping. + * + * @param prefix + * the prefix that was being mapped. This is the empty string + * when a default mapping scope ends. + * @param uri + * the Namespace URI the prefix is mapped to + */ + void endPrefixMapping(String prefix, String uri); + + /** + * Receive notification of the beginning of an element. + * + * @param uri + * the Namespace URI, or the empty string if the element has no + * Namespace URI or if Namespace processing is not being + * performed + * @param localName + * the local name (without prefix), or the empty string if + * Namespace processing is not being performed + * @param qName + * the qualified name (with prefix), or the empty string if + * qualified names are not available + * @param atts + * the attributes attached to the element. If there are no + * attributes, it shall be an empty Attributes object. The value + * of this object after startElement returns is undefined + */ + void startElement(String uri, String localName, String qName, + Attribute[] atts); + + /** + * Receive notification of the end of an element. + * + * @param uri + * the Namespace URI, or the empty string if the element has no + * Namespace URI or if Namespace processing is not being + * performed + * @param localName + * the local name (without prefix), or the empty string if + * Namespace processing is not being performed + * @param qName + * the qualified XML name (with prefix), or the empty string if + * qualified names are not available + */ + void endElement(String uri, String localName, String qName); + + /** + * Receive notification of text. + * + * @param data + * the text data + */ + void text(String data); + + /** + * Receive notification of character data (in a <![CDATA[ ]]> block). + * + * @param data + * the text data + */ + void characterData(String data); + + /** + * Receive notification of a processing instruction. + * + * @param target + * the processing instruction target + * @param data + * the processing instruction data, or null if none was supplied. + * The data does not include any whitespace separating it from + * the target + */ + void processingInstruction(String target, String data); +} diff --git a/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlUtils.java b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlUtils.java new file mode 100644 index 000000000..5e3d24b31 --- /dev/null +++ b/libs/AXML/src/main/java/fr/xgouchet/axml/CompressedXmlUtils.java @@ -0,0 +1,60 @@ +package fr.xgouchet.axml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +public final class CompressedXmlUtils { + + /** + * @param input an input stream + * @return if the given input stream looks like an AXML document + */ + public static boolean isCompressedXml(final InputStream input) { + if (input == null) return false; + + boolean result; + + try { + final byte[] header = new byte[4]; + int read = input.read(header, 0, 4); + if (read < 4) return false; + + result = (header[0] == 0x03) + && (header[1] == 0x00) + && (header[2] == 0x08) + && (header[3] == 0x00); + + } catch (Exception e) { + result = false; + } finally { + try { + input.close(); + } catch (Exception e) { + // ignore this exception + } + } + + return result; + } + + /** + * @param source a source file + * @return if the given file looks like an AXML file + */ + public static boolean isCompressedXml(final File source) { + boolean result; + + try { + final InputStream input = new FileInputStream(source.getPath()); + result = isCompressedXml(input); + } catch (Exception e) { + result = false; + } + + return result; + } + + private CompressedXmlUtils() { + } +} diff --git a/settings.gradle b/settings.gradle index 4f5581693..609b97313 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,3 @@ -include ':libs:android-transcoder' +include ':libs:AXML' include ':libs:xmpp-addr' rootProject.name = 'monocles chat' diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index 92187f0b0..96d5dca01 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -122,19 +122,19 @@ public class AbstractConnectionManager { final long defaultValue_mobile = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_mobile); final long defaultValue_roaming = this.getXmppConnectionService().getResources().getInteger(R.integer.auto_accept_filesize_roaming); - long config = 0; + String config = "0"; if (mXmppConnectionService.isWIFI()) { - config = this.mXmppConnectionService.getPreferences().getLong( - "auto_accept_file_size_wifi", defaultValue_wifi); + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_wifi", String.valueOf(defaultValue_wifi)); } else if (mXmppConnectionService.isMobile()) { - config = this.mXmppConnectionService.getPreferences().getLong( - "auto_accept_file_size_mobile", defaultValue_mobile); + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_mobile", String.valueOf(defaultValue_mobile)); } else if (mXmppConnectionService.isMobileRoaming()) { - config = this.mXmppConnectionService.getPreferences().getLong( - "auto_accept_file_size_roaming", defaultValue_roaming); + config = this.mXmppConnectionService.getPreferences().getString( + "auto_accept_file_size_roaming", String.valueOf(defaultValue_roaming)); } try { - return config <= 0 ? -1 : config; + return Long.parseLong(config) <= 0 ? -1 : Long.parseLong(config); } catch (NumberFormatException e) { return defaultValue_mobile; } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 59dbd36c6..a19921dcb 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -22,6 +22,7 @@ import android.app.Fragment; import android.app.FragmentManager; import android.app.PendingIntent; import android.content.ActivityNotFoundException; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -143,6 +144,7 @@ import eu.siacs.conversations.ui.util.ShareUtil; import eu.siacs.conversations.ui.util.StyledAttributes; import eu.siacs.conversations.ui.util.ViewUtil; import eu.siacs.conversations.ui.widget.EditMessage; +import eu.siacs.conversations.utils.CameraUtils; import eu.siacs.conversations.utils.Compatibility; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.MenuDoubleTabUtil; @@ -2077,12 +2079,30 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke intent.setAction(Intent.ACTION_GET_CONTENT); break; case ATTACHMENT_CHOICE_RECORD_VIDEO: + if (Compatibility.runsThirty()) { + final List cameraApps = CameraUtils.getCameraApps(activity); + if (cameraApps.size() == 0) { + ToastCompat.makeText(activity, R.string.no_application_found, ToastCompat.LENGTH_LONG).show(); + } else { + final ComponentName correctComponent = cameraApps.get(0).componentNames.get(0); + intent.setComponent(correctComponent); + } + } intent.setAction(MediaStore.ACTION_VIDEO_CAPTURE); break; case ATTACHMENT_CHOICE_TAKE_PHOTO: - final Uri uri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri(); - pendingTakePhotoUri.push(uri); - intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + final Uri photoUri = activity.xmppConnectionService.getFileBackend().getTakePhotoUri(); + pendingTakePhotoUri.push(photoUri); + if (Compatibility.runsThirty()) { + final List cameraApps = CameraUtils.getCameraApps(activity); + if (cameraApps.size() == 0) { + ToastCompat.makeText(activity, R.string.no_application_found, ToastCompat.LENGTH_LONG).show(); + } else { + final ComponentName correctComponent = cameraApps.get(0).componentNames.get(0); + intent.setComponent(correctComponent); + } + } + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION & Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); break; diff --git a/src/main/java/eu/siacs/conversations/utils/CameraUtils.java b/src/main/java/eu/siacs/conversations/utils/CameraUtils.java new file mode 100644 index 000000000..3e3cfb979 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/CameraUtils.java @@ -0,0 +1,130 @@ +package eu.siacs.conversations.utils; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.provider.MediaStore; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import fr.xgouchet.axml.CompressedXmlParser; + +public class CameraUtils { + + public PackageInfo packageInfo; + public List componentNames; + + public CameraUtils(PackageInfo packageInfo, List componentNames) { + this.packageInfo = packageInfo; + this.componentNames = componentNames; + } + + + public static List getCameraApps(Context context) { + //Step 1 - Get apps with Camera permission + List cameraPermissionPackages = getPackageInfosWithCameraPermission(context); + //Step 2 - Filter out apps with the correct intent-filter(s) + List cameraApps = new ArrayList(); + for (PackageInfo somePackage : cameraPermissionPackages) { + try { + //Step 2a - Get the AndroidManifest.xml + Document doc = readAndroidManifestFromPackageInfo(somePackage); + //Step 2b - Get Camera ComponentNames from Manifest + List componentNames = getCameraComponentNamesFromDocument(doc); + if (componentNames.size() == 0) { + continue; //This is not a Camera app + } + //Step 2c - Create CameraAppModel + CameraUtils cameraApp = new CameraUtils(somePackage, componentNames); + cameraApps.add(cameraApp); + } catch (Exception e) { + //ignore + } + } + return cameraApps; + } + + public static List getPackageInfosWithCameraPermission(Context context) { + //Get a list of compatible apps + PackageManager pm = context.getPackageManager(); + List installedPackages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS); + ArrayList cameraPermissionPackages = new ArrayList(); + //filter out only camera apps + for (PackageInfo somePackage : installedPackages) { + //- A camera app should have the Camera permission + boolean hasCameraPermission = false; + if (somePackage.requestedPermissions == null || somePackage.requestedPermissions.length == 0) { + continue; + } + for (String requestPermission : somePackage.requestedPermissions) { + if (requestPermission.equals(Manifest.permission.CAMERA)) { + //Ask for Camera permission, now see if it's granted. + if (pm.checkPermission(Manifest.permission.CAMERA, somePackage.packageName) == PackageManager.PERMISSION_GRANTED) { + hasCameraPermission = true; + break; + } + } + } + if (hasCameraPermission) { + cameraPermissionPackages.add(somePackage); + } + } + return cameraPermissionPackages; + } + + public static Document readAndroidManifestFromPackageInfo(PackageInfo packageInfo) throws IOException { + File apkFile = new File(packageInfo.applicationInfo.publicSourceDir); + //Get AndroidManifest.xml from APK + ZipFile apkZipFile = new ZipFile(apkFile, ZipFile.OPEN_READ); + ZipEntry manifestEntry = apkZipFile.getEntry("AndroidManifest.xml"); + InputStream manifestInputStream = apkZipFile.getInputStream(manifestEntry); + try { + return new CompressedXmlParser().parseDOM(manifestInputStream); + } catch (Exception e) { + throw new IOException("Error reading AndroidManifest", e); + } + } + + public static List getCameraComponentNamesFromDocument(Document doc) { + @SuppressLint("InlinedApi") + String[] correctActions = {MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_IMAGE_CAPTURE_SECURE, MediaStore.ACTION_VIDEO_CAPTURE}; + ArrayList componentNames = new ArrayList(); + Element manifestElement = (Element) doc.getElementsByTagName("manifest").item(0); + String packageName = manifestElement.getAttribute("package"); + Element applicationElement = (Element) manifestElement.getElementsByTagName("application").item(0); + NodeList activities = applicationElement.getElementsByTagName("activity"); + for (int i = 0; i < activities.getLength(); i++) { + Element activityElement = (Element) activities.item(i); + String activityName = activityElement.getAttribute("android:name"); + NodeList intentFiltersList = activityElement.getElementsByTagName("intent-filter"); + for (int j = 0; j < intentFiltersList.getLength(); j++) { + Element intentFilterElement = (Element) intentFiltersList.item(j); + NodeList actionsList = intentFilterElement.getElementsByTagName("action"); + for (int k = 0; k < actionsList.getLength(); k++) { + Element actionElement = (Element) actionsList.item(k); + String actionName = actionElement.getAttribute("android:name"); + for (String correctAction : correctActions) { + if (actionName.equals(correctAction)) { + //this activity has an intent filter with a correct action, add this to the list. + componentNames.add(new ComponentName(packageName, activityName)); + } + } + } + } + } + return componentNames; + } +}