forked from mirror/monocles_chat
fix taking photos/recording videos from camera for Android 11 [Christian Schneppe]
This commit is contained in:
parent
a8857bec68
commit
46792bdfb8
13 changed files with 1046 additions and 12 deletions
|
@ -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')
|
||||
}
|
||||
|
||||
|
|
1
libs/AXML/.gitignore
vendored
Normal file
1
libs/AXML/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
31
libs/AXML/build.gradle
Normal file
31
libs/AXML/build.gradle
Normal file
|
@ -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
|
||||
}
|
17
libs/AXML/proguard-rules.pro
vendored
Normal file
17
libs/AXML/proguard-rules.pro
vendored
Normal file
|
@ -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 *;
|
||||
#}
|
66
libs/AXML/src/main/java/fr/xgouchet/axml/Attribute.java
Normal file
66
libs/AXML/src/main/java/fr/xgouchet/axml/Attribute.java
Normal file
|
@ -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;
|
||||
}
|
|
@ -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<Node> mStack;
|
||||
private Document mDocument;
|
||||
private final DocumentBuilder mBuilder;
|
||||
}
|
|
@ -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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00080003</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* </ul>
|
||||
*/
|
||||
private void parseStartDocument() {
|
||||
mListener.startDocument();
|
||||
mParserOffset += (2 * WORD_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* the string table starts with the following 4bytes words :
|
||||
* <ul>
|
||||
* <li>0th word : 0x1c0001</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* <li>2nd word : number of string in the string table</li>
|
||||
* <li>3rd word : number of styles in the string table</li>
|
||||
* <li>4th word : flags - sorted/utf8 flag (0)</li>
|
||||
* <li>5th word : Offset to String data</li>
|
||||
* <li>6th word : Offset to style data</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00080180</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00100100 = Start NS / 0x00100101 = end NS</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* <li>2nd word : line this tag appeared</li>
|
||||
* <li>3rd word : optional xml comment for element (usually 0xFFFFFF)</li>
|
||||
* <li>4th word : index of namespace prefix in StringIndexTable</li>
|
||||
* <li>5th word : index of namespace uri in StringIndexTable</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00100102 = Start_Tag</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* <li>2nd word : line this tag appeared in the original file</li>
|
||||
* <li>3rd word : optional xml comment for element (usually 0xFFFFFF)</li>
|
||||
* <li>4th word : index of namespace uri in StringIndexTable, or 0xFFFFFFFF
|
||||
* for default NS</li>
|
||||
* <li>5th word : index of element name in StringIndexTable</li>
|
||||
* <li>6th word : size of attribute structures to follow</li>
|
||||
* <li>7th word : number of attributes following the start tag</li>
|
||||
* <li>8th word : index of id attribute (0 if none)</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : index of namespace uri in StringIndexTable, or 0xFFFFFFFF
|
||||
* for default NS</li>
|
||||
* <li>1st word : index of attribute name in StringIndexTable</li>
|
||||
* <li>2nd word : index of attribute value, or 0xFFFFFFFF if value is a
|
||||
* typed value</li>
|
||||
* <li>3rd word : value type</li>
|
||||
* <li>4th word : resource id value</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00100104 = Text</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* <li>2nd word : line this element appeared in the original document</li>
|
||||
* <li>3rd word : optional xml comment for element (usually 0xFFFFFF)</li>
|
||||
* <li>4rd word : string index in string table</li>
|
||||
* <li>5rd word : ??? (always 8)</li>
|
||||
* <li>6rd word : ??? (always 0)</li>
|
||||
* </ul>
|
||||
*/
|
||||
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 :
|
||||
* <ul>
|
||||
* <li>0th word : 0x00100103 = End_Tag</li>
|
||||
* <li>1st word : chunk size</li>
|
||||
* <li>2nd word : line this tag appeared in the original file</li>
|
||||
* <li>3rd word : optional xml comment for element (usually 0xFFFFFF)</li>
|
||||
* <li>4th word : index of namespace name in StringIndexTable, or 0xFFFFFFFF
|
||||
* for default NS</li>
|
||||
* <li>5th word : index of element name in StringIndexTable</li>
|
||||
* </ul>
|
||||
*/
|
||||
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<String, String> mNamespaces;
|
||||
private byte[] mData;
|
||||
|
||||
private String[] mStringsTable;
|
||||
private int[] mResourcesIds;
|
||||
private int mStringsCount, mStylesCount, mResCount;
|
||||
private int mParserOffset;
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
include ':libs:android-transcoder'
|
||||
include ':libs:AXML'
|
||||
include ':libs:xmpp-addr'
|
||||
rootProject.name = 'monocles chat'
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<CameraUtils> 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<CameraUtils> 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;
|
||||
|
|
130
src/main/java/eu/siacs/conversations/utils/CameraUtils.java
Normal file
130
src/main/java/eu/siacs/conversations/utils/CameraUtils.java
Normal file
|
@ -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<ComponentName> componentNames;
|
||||
|
||||
public CameraUtils(PackageInfo packageInfo, List<ComponentName> componentNames) {
|
||||
this.packageInfo = packageInfo;
|
||||
this.componentNames = componentNames;
|
||||
}
|
||||
|
||||
|
||||
public static List<CameraUtils> getCameraApps(Context context) {
|
||||
//Step 1 - Get apps with Camera permission
|
||||
List<PackageInfo> cameraPermissionPackages = getPackageInfosWithCameraPermission(context);
|
||||
//Step 2 - Filter out apps with the correct intent-filter(s)
|
||||
List<CameraUtils> cameraApps = new ArrayList<CameraUtils>();
|
||||
for (PackageInfo somePackage : cameraPermissionPackages) {
|
||||
try {
|
||||
//Step 2a - Get the AndroidManifest.xml
|
||||
Document doc = readAndroidManifestFromPackageInfo(somePackage);
|
||||
//Step 2b - Get Camera ComponentNames from Manifest
|
||||
List<ComponentName> 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<PackageInfo> getPackageInfosWithCameraPermission(Context context) {
|
||||
//Get a list of compatible apps
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<PackageInfo> installedPackages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS);
|
||||
ArrayList<PackageInfo> cameraPermissionPackages = new ArrayList<PackageInfo>();
|
||||
//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<ComponentName> getCameraComponentNamesFromDocument(Document doc) {
|
||||
@SuppressLint("InlinedApi")
|
||||
String[] correctActions = {MediaStore.ACTION_IMAGE_CAPTURE, MediaStore.ACTION_IMAGE_CAPTURE_SECURE, MediaStore.ACTION_VIDEO_CAPTURE};
|
||||
ArrayList<ComponentName> componentNames = new ArrayList<ComponentName>();
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue