Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2021-2024 MIT, All rights reserved
// Copyright 2021-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -37,6 +37,7 @@ public class ComponentInfo {
private final ConcurrentMap<String, Set<String>> servicesNeeded;
private final ConcurrentMap<String, Set<String>> contentProvidersNeeded;
private final ConcurrentMap<String, Set<String>> xmlsNeeded;
private final ConcurrentMap<String, Set<String>> featuresNeeded;

private Set<String> uniqueLibsNeeded;
private AARLibraries explodedAarLibs;
Expand All @@ -60,6 +61,7 @@ public ComponentInfo() {
servicesNeeded = new ConcurrentHashMap<>();
contentProvidersNeeded = new ConcurrentHashMap<>();
xmlsNeeded = new ConcurrentHashMap<>();
featuresNeeded = new ConcurrentHashMap<>();

uniqueLibsNeeded = Sets.newHashSet();
}
Expand Down Expand Up @@ -141,6 +143,10 @@ public ConcurrentMap<String, Set<String>> getXmlsNeeded() {
return xmlsNeeded;
}

public ConcurrentMap<String, Set<String>> getFeaturesNeeded() {
return featuresNeeded;
}

@Override
public String toString() {
return "JsonInfo{"
Expand All @@ -154,6 +160,7 @@ public String toString() {
+ ", componentBroadcastReceiver=" + componentBroadcastReceiver
+ ", uniqueLibsNeeded=" + uniqueLibsNeeded
+ ", xmlsNeeded=" + xmlsNeeded
+ ", featuresNeeded=" + featuresNeeded
+ '}';
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2021-2023 MIT, All rights reserved
// Copyright 2021-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -27,6 +27,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -108,6 +109,27 @@ public TaskResult execute(AndroidCompilerContext context) {
}
}

final Map<String, Set<String>> featuresNeeded = context.getComponentInfo().getFeaturesNeeded();
if (!featuresNeeded.isEmpty()) {
final HashMap<String, String> uniqueFeatures = new HashMap<>();
for (Map.Entry<String, Set<String>> componentSubElSetPair : featuresNeeded.entrySet()) {
for (String feature : componentSubElSetPair.getValue()) {
// Extract just the android:name value
String name = feature.replaceAll(".*android:name=\"([^\"]+)\".*", "$1");
boolean isRequiredTrue = feature.contains("android:required=\"true\"");

// If new, store it — if existing, only replace if this one has required="true"
if (!uniqueFeatures.containsKey(name) || isRequiredTrue) {
uniqueFeatures.put(name, feature);
}
}
}

for (String feature : uniqueFeatures.values()) {
out.write(feature);
}
}

final Map<String, Set<String>> queriesNeeded = context.getComponentInfo().getQueriesNeeded();
if (!queriesNeeded.isEmpty()) {
out.write(" <queries>\n");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2021-2024 MIT, All rights reserved
// Copyright 2021-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -70,7 +70,8 @@ public TaskResult execute(CompilerContext<?> context) {
|| !this.generatePermissions()
|| !this.generateQueries()
|| !this.generateServices()
|| !this.generateXmls()) {
|| !this.generateXmls()
|| !this.generateFeatures()) {
return TaskResult.generateError("Could not extract info from the app");
}

Expand Down Expand Up @@ -276,6 +277,28 @@ private boolean generateXmls() {
return true;
}

/**
* Generate a set of conditionally included uses-features needed by this project.
*/
private boolean generateFeatures() {
try {
loadJsonInfo(context.getComponentInfo().getFeaturesNeeded(),
ComponentDescriptorConstants.FEATURES_TARGET);
} catch (IOException | JSONException e) {
// This is fatal.
context.getReporter().error("There was an error in the Features stage", true);
return false;
}

int n = 0;
for (String type : context.getComponentInfo().getFeaturesNeeded().keySet()) {
n += context.getComponentInfo().getFeaturesNeeded().get(type).size();
}

context.getReporter().log("Component features needed, n = " + n);
return true;
}

/*
* Generate the set of Android libraries needed by this project.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.annotations;

import com.google.appinventor.components.annotations.androidmanifest.FeatureElement;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Declares hardware or software features that is used by a component.
*
* @author https://github.com/jewelshkjony (Jewel Shikder Jony)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UsesFeatures {

/**
* The values of the features (as an array)
*
* @return the array of elements data
*/
FeatureElement[] features();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.components.annotations.androidmanifest;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to describe an <uses-feature> element required by a component
* so that it can be added to AndroidManifest.xml.
*
* @author https://github.com/jewelshkjony (Jewel Shikder Jony)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FeatureElement {

/**
* Specifies a single hardware or software feature used by the application as a descriptor string.
* Valid attribute values are listed in the
* <a href="https://developer.android.com/guide/topics/manifest/uses-feature-element#hw-features">Hardware features</a>
* and <a href="https://developer.android.com/guide/topics/manifest/uses-feature-element#sw-features">Software features</a> sections.
* These attribute values are case-sensitive.
*
* @return the name of the element
*/
String name();

/**
* Boolean value that indicates whether the application requires the featureContents of the xml file.
* Declaring true for a feature indicates that the application can't function,
* or isn't designed to function, when the specified feature isn't present on the device.
* Declaring false for a feature indicates that the application uses the feature if present on the device,
* but that it is designed to function without the specified feature if necessary.
*
* @return Returns true if required otherwise false
*/
boolean required() default true;

/**
* The OpenGL ES version required by the application. The higher 16 bits represent the major
* number and the lower 16 bits represent the minor number. For example, to specify OpenGL
* ES version 2.0, you set the value as "0x00020000", or to specify OpenGL ES 3.2,
* you set the value as "0x00030002".
*
* @return the glEs version
*/
int glEsVersion() default -1;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2011-2024 MIT, All rights reserved
// Copyright 2011-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -33,6 +33,7 @@ private ComponentDescriptorConstants() {
public static final String ANDROIDMINSDK_TARGET = "androidMinSdk";
public static final String CONDITIONALS_TARGET = "conditionals";
public static final String XMLS_TARGET = "xmls";
public static final String FEATURES_TARGET = "features";

// TODO(Will): Remove the following target once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2024 MIT, All rights reserved
// Copyright 2011-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -89,6 +89,7 @@ private static JSONObject outputComponentBuildInfo(ComponentInfo component) {
appendComponentInfo(json, ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET,
component.contentProviders);
appendComponentInfo(json, ComponentDescriptorConstants.XMLS_TARGET, component.xmls);
appendComponentInfo(json, ComponentDescriptorConstants.FEATURES_TARGET, component.features);
appendConditionalComponentInfo(component, json);
// TODO(Will): Remove the following call once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2024 MIT, All rights reserved
// Copyright 2011-2025 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

Expand Down Expand Up @@ -29,6 +29,8 @@
import com.google.appinventor.components.annotations.UsesServices;
import com.google.appinventor.components.annotations.UsesXmls;
import com.google.appinventor.components.annotations.XmlElement;
import com.google.appinventor.components.annotations.UsesFeatures;
import com.google.appinventor.components.annotations.androidmanifest.FeatureElement;
import com.google.appinventor.components.annotations.androidmanifest.ActivityElement;
import com.google.appinventor.components.annotations.androidmanifest.ReceiverElement;
import com.google.appinventor.components.annotations.androidmanifest.IntentFilterElement;
Expand Down Expand Up @@ -170,7 +172,8 @@ public abstract class ComponentProcessor extends AbstractProcessor {
"com.google.appinventor.components.annotations.UsesQueries",
"com.google.appinventor.components.annotations.UsesServices",
"com.google.appinventor.components.annotations.UsesContentProviders",
"com.google.appinventor.components.annotations.UsesXmls");
"com.google.appinventor.components.annotations.UsesXmls",
"com.google.appinventor.components.annotations.UsesFeature");

// Returned by getRwString()
private static final String READ_WRITE = "read-write";
Expand Down Expand Up @@ -1188,6 +1191,11 @@ protected final class ComponentInfo extends Feature {
*/
protected final Set<String> xmls;

/**
* Uses features required by this component.
*/
protected final Set<String> features;

/**
* TODO(Will): Remove the following field once the deprecated {@link SimpleBroadcastReceiver}
* annotation is removed. It should should remain for the time being
Expand Down Expand Up @@ -1281,6 +1289,7 @@ protected ComponentInfo(Element element) {
queries = Sets.newHashSet();
services = Sets.newHashSet();
xmls = Sets.newHashSet();
features = Sets.newHashSet();

designerProperties = Maps.newTreeMap();
properties = Maps.newTreeMap();
Expand Down Expand Up @@ -1648,6 +1657,7 @@ private void processComponent(Element element) {
componentInfo.services.addAll(parentComponent.services);
componentInfo.contentProviders.addAll(parentComponent.contentProviders);
componentInfo.xmls.addAll(parentComponent.xmls);
componentInfo.features.addAll(parentComponent.features);
// TODO(Will): Remove the following call once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
Expand Down Expand Up @@ -1861,6 +1871,22 @@ private void processComponent(Element element) {
}
}

// Gather the required uses-feature elements and build their element strings.
UsesFeatures usesFeatures = element.getAnnotation(UsesFeatures.class);
if (usesFeatures != null) {
final Map<String, FeatureElement> featureMap = new HashMap<>();
for (FeatureElement fe : usesFeatures.features()) {
if (!featureMap.containsKey(fe.name())) {
featureMap.put(fe.name(), fe);
} else if (fe.required()){
featureMap.put(fe.name(), fe);
}
}
for (FeatureElement fe : featureMap.values()) {
updateWithNonEmptyValue(componentInfo.features, featureElementToString(fe));
}
}

// TODO(Will): Remove the following legacy code once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
Expand Down Expand Up @@ -2417,6 +2443,23 @@ private static String xmlElementToString(XmlElement element) {
return elementString.append(element.content()).toString();
}

// Transform a @FeatureElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String featureElementToString(FeatureElement element) {
StringBuilder elementString = new StringBuilder(" <uses-feature android:name=\"");
elementString.append(element.name());
elementString.append("\" android:required=\"");
elementString.append(element.required());
elementString.append("\" ");
if (element.glEsVersion() != -1) {
elementString.append("android:glEsVersion=\"");
elementString.append(element.glEsVersion());
elementString.append("\" ");
}
elementString.append("/>\n");
return elementString.toString();
}

// Transform a @ProviderElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String providerElementToString(ProviderElement element)
Expand Down