Unverified Commit c503528d authored by Shreyash Saitwal's avatar Shreyash Saitwal Committed by GitHub

Implement annotations for Services and Content Providers (#2217)

parent 1843bdd6
......@@ -170,6 +170,10 @@ public final class Compiler {
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> broadcastReceiversNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> servicesNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> contentProvidersNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> libsNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> nativeLibsNeeded =
......@@ -420,6 +424,18 @@ public final class Compiler {
return broadcastReceiversNeeded;
}
// Just used for testing
@VisibleForTesting
Map<String, Set<String>> getServices() {
return servicesNeeded;
}
// Just used for testing
@VisibleForTesting
Map<String, Set<String>> getContentProviders() {
return contentProvidersNeeded;
}
// Just used for testing
@VisibleForTesting
Map<String, Set<String>> getActivities() {
......@@ -600,6 +616,46 @@ public final class Compiler {
mergeConditionals(conditionals.get(ComponentDescriptorConstants.BROADCAST_RECEIVERS_TARGET), broadcastReceiversNeeded);
}
/*
* Generate a set of conditionally included services needed by this project.
*/
@VisibleForTesting
void generateServices() {
try {
loadJsonInfo(servicesNeeded, ComponentDescriptorConstants.SERVICES_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Services"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Services"));
}
mergeConditionals(conditionals.get(ComponentDescriptorConstants.SERVICES_TARGET), servicesNeeded);
}
/*
* Generate a set of conditionally included content providers needed by this project.
*/
@VisibleForTesting
void generateContentProviders() {
try {
loadJsonInfo(contentProvidersNeeded, ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Content Providers"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Content Providers"));
}
mergeConditionals(conditionals.get(ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET), contentProvidersNeeded);
}
/*
* TODO(Will): Remove this method once the deprecated @SimpleBroadcastReceiver
* annotation is removed. This should remain for the time being so
......@@ -1119,10 +1175,13 @@ public final class Compiler {
subelements.addAll(activitiesNeeded.entrySet());
subelements.addAll(metadataNeeded.entrySet());
subelements.addAll(broadcastReceiversNeeded.entrySet());
subelements.addAll(servicesNeeded.entrySet());
subelements.addAll(contentProvidersNeeded.entrySet());
// If any component needs to register additional activities or
// broadcast receivers, insert them into the manifest here.
// If any component needs to register additional activities,
// broadcast receivers, services or content providers, insert
// them into the manifest here.
if (!subelements.isEmpty()) {
for (Map.Entry<String, Set<String>> componentSubElSetPair : subelements) {
Set<String> subelementSet = componentSubElSetPair.getValue();
......@@ -1234,6 +1293,8 @@ public final class Compiler {
compiler.generateMetadata();
compiler.generateActivityMetadata();
compiler.generateBroadcastReceivers();
compiler.generateServices();
compiler.generateContentProviders();
compiler.generateLibNames();
compiler.generateNativeLibNames();
compiler.generatePermissions();
......@@ -2514,7 +2575,8 @@ public final class Compiler {
* @param type The name of the type being processed
* @param targetInfo Name of the annotation target being processed (e.g.,
* permissions). Any of: PERMISSIONS_TARGET,
* BROADCAST_RECEIVERS_TARGET
* BROADCAST_RECEIVERS_TARGET, SERVICES_TARGET,
* CONTENT_PROVIDERS_TARGET
*/
private void processConditionalInfo(JSONObject compJson, String type, String targetInfo) {
// Strip off the package name since SCM and BKY use unqualified names
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2020 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.ProviderElement;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate any content providers used by
* a component so that corresponding <provider> elements can be
* created in AndroidManifest.xml.
*
* @author https://github.com/ShreyashSaitwal (Shreyash Saitwal)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface UsesContentProviders {
/**
* An array containing each {@link ProviderElement}
* that is required by the component.
*
* @return the array containing the relevant providers
*/
ProviderElement[] providers();
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2020 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.ServiceElement;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to indicate any services used by
* a component so that corresponding <service> elements can be
* created in AndroidManifest.xml.
*
* @author https://github.com/ShreyashSaitwal (Shreyash Saitwal)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface UsesServices {
/**
* An array containing each {@link ServiceElement}
* that is required by the component.
*
* @return the array containing the relevant services
*/
ServiceElement[] services();
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2020 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;
/**
* Specifies the subsets of app data that the parent content provider has permission to access.
* Data subsets are indicated by the path part of a content: URI. (The authority part of the URI
* identifies the content provider.) Granting permission is a way of enabling clients of the <provider>
* that don't normally have permission to access its data to overcome that restriction on a one-time
* basis.
*
* If a content provider's grantUriPermissions attribute is "true", permission can be granted for
* any the data under the provider's purview. However, if that attribute is "false", permission can
* be granted only to data subsets that are specified by this element. A provider can contain any
* number of <grant-uri-permission> elements. Each one can specify only one path (only one of the
* three possible attributes).
*
* Note: Most of this documentation is adapted from the Android framework specification
* linked below. That documentation is licensed under the
* {@link <a href="https://creativecommons.org/licenses/by/2.5/">
* Creative Commons Attribution license v2.5
* </a>}.
*
* See {@link <a href="https://developer.android.com/guide/topics/manifest/grant-uri-permission-element">
* https://developer.android.com/guide/topics/manifest/grant-uri-permission-element
* </a>}.
*
* @author https://github.com/ShreyashSaitwal (Shreyash Saitwal)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface GrantUriPermissionElement {
/**
* A complete URI path for a subset of content provider data. Permission can be granted only
* to the particular data identified by this path. When used to provide search suggestion
* content, it must be appended with "/search_suggest_query".
*
* @return the grant uri permission path attribute
*/
String path() default "";
/**
* The initial part of a URI path for a subset of content provider data. Permission can be
* granted to all data subsets with paths that share this initial part.
*
* @return the grant uri permission pathPrefix attribute
*/
String pathPrefix() default "";
/**
* A path identifying the data subset or subsets that permission can be granted for. The path
* attribute specifies a complete path; permission can be granted only to the particular data
* subset identified by that path. The pathPrefix attribute specifies the initial part of a path;
* permission can be granted to all data subsets with paths that share that initial part. The
* pathPattern attribute specifies a complete path, but one that can contain the following wildcards:
* - An asterisk ('*') matches a sequence of 0 to many occurrences of the immediately preceding
* character.
* - A period followed by an asterisk (".*") matches any sequence of 0 to many characters.
* Because '\' is used as an escape character when the string is read from XML (before it is parsed
* as a pattern), you will need to double-escape: For example, a literal '*' would be written as "\\*"
* and a literal '\' would be written as "\\\\". This is basically the same as what you would need to
* write if constructing the string in Java code.
*
* @return the grant uri permission pathPattern attribute respectively
*/
String pathPattern() default "";
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2020 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;
/**
* Defines the path and required permissions for a specific subset of data within a
* <provider>. This element can be specified multiple times to supply multiple paths.
*
* Note: Most of this documentation is adapted from the Android framework specification
* linked below. That documentation is licensed under the
* {@link <a href="https://creativecommons.org/licenses/by/2.5/">
* Creative Commons Attribution license v2.5
* </a>}.
*
* See {@link <a href="https://developer.android.com/guide/topics/manifest/path-permission-element">
* https://developer.android.com/guide/topics/manifest/path-permission-element
* </a>}.
*
* @author https://github.com/ShreyashSaitwal (Shreyash Saitwal)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PathPermissionElement {
/**
* A complete URI path for a subset of content provider data. Permission can be granted only
* to the particular data identified by this path. When used to provide search suggestion
* content, it must be appended with "/search_suggest_query".
*
* @return the path permission path attribute
*/
String path() default "";
/**
* The initial part of a URI path for a subset of content provider data. Permission can be
* granted to all data subsets with paths that share this initial part.
*
* @return the path permission pathPrefix attribute
*/
String pathPrefix() default "";
/**
* A complete URI path for a subset of content provider data, but one that can use the following
* wildcards:
* - An asterisk ('*'). This matches a sequence of 0 to many occurrences of the immediately
* preceding character.
* - A period followed by an asterisk (".*"). This matches any sequence of 0 or more characters.
*
* Because '\' is used as an escape character when the string is read from XML (before it is parsed
* as a pattern), you will need to double-escape. For example, a literal '*' would be written as "\\*"
* and a literal '\' would be written as "\\". This is basically the same as what you would need to
* write if constructing the string in Java code.
*
* @return the path permission pathPattern attribute
*/
String pathPattern() default "";
/**
* The name of a permission that clients must have in order to read or write the content provider's
* data. This attribute is a convenient way of setting a single permission for both reading and
* writing. However, the {@link #readPermission()} and {@link #writePermission()} attributes take
* precedence over this one.
*
* @return the path permission permission attribute
*/
String permission() default "";
/**
* A permission that clients must have in order to query the content provider.
*
* @return the path permission readPermission attribute
*/
String readPermission() default "";
/**
* A permission that clients must have in order to make changes to the data controlled by the
* content provider.
*
* @return the path permission writePermission attribute
*/
String writePermission() default "";
}
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2020 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 a <service> element required by a component so that
* it can be added to AndroidManifest.xml. <service> elements indicate that
* a component is a service. <service> element attributes that are not
* set explicitly default to "" or {} and are ignored when the element is created
* in the manifest.
*
* Note: Most of this documentation is adapted from the Android framework specification
* linked below. That documentation is licensed under the
* {@link <a href="https://creativecommons.org/licenses/by/2.5/">
* Creative Commons Attribution license v2.5
* </a>}.
*
* See {@link <a href="https://developer.android.com/guide/topics/manifest/service-element">
* https://developer.android.com/guide/topics/manifest/service-element
* </a>}.
*
* @author https://github.com/ShreyashSaitwal (Shreyash Saitwal)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServiceElement {
/**
* An array containing any intent filters used by this <service> element.
*
* @return an array containing the <intent-filter> subelements for this
* <service> element
*/
IntentFilterElement[] intentFilters() default {};
/**
* An array containing any meta data used by this <service> element.
*
* @return an array containing the <meta-data> subelements for this
* <service> element
*/
MetaDataElement[] metaDataElements() default {};
/**
* The name of the class that implements the service. This should
* be a fully qualified class name (such as, "com.example.project.RoomService").
* However, as a shorthand, if the first character of the name is
* a period (for example, ".RoomService"), it is appended to the
* package name of the application.
*
* @return the Service class name
*/
String name();
/**
* If set to true, this service will run under a special process that
* is isolated from the rest of the system and has no permissions
* of its own. The only communication with it is through the Service
* API (binding and starting).
*
* @return the service isolatedProcess attribute
*/
String isolatedProcess() default "";
/**
* Specify that the service is a foreground service that satisfies a
* particular use case. For example, a foreground service type of
* "location" indicates that an app is getting the device's current
* location, usually to continue a user-initiated action related to
* device location.
* You can assign multiple foreground service types to a particular service.
*
* @return the service foregroundServiceType attribute
*/
String foregroundServiceType() default "";
/**
* A string that describes the service to users. The label should be
* set as a reference to a string resource, so that it can be localized
* like other strings in the user interface.
*
* @return the service desciption attribute
*/
String description() default "";
/**
* Whether or not the service is direct-boot aware; that is, whether or
* not it can run before the user unlocks the device.
*
* @return the service directBootAware attribute
*/
String directBootAware() default "";
/**
* Whether or not the service can be instantiated by the system — "true"
* if it can be, and "false" if not. The default value is "true".
* The <application> element has its own enabled attribute that applies
* to all application components, including services. The <application>
* and <service> attributes must both be "true" (as they both are by default)
* for the service to be enabled. If either is "false", the service is
* disabled; it cannot be instantiated.
*
* @return the service enabled attribute
*/
String enabled() default "";
/**
* Whether or not components of other applications can invoke the service
* or interact with it — "true" if they can, and "false" if not. When the
* value is "false", only components of the same application or applications
* with the same user ID can start the service or bind to it.
*
* The default value depends on whether the service contains intent filters.
* The absence of any filters means that it can be invoked only by specifying
* its exact class name. This implies that the service is intended only for
* application-internal use (since others would not know the class name). So
* in this case, the default value is "false". On the other hand, the presence
* of at least one filter implies that the service is intended for external use,
* so the default value is "true".
* This attribute is not the only way to limit the exposure of a service to other
* applications. You can also use a permission to limit the external entities that
* can interact with the service.
*
* @return the service exported attribute
*/
String exported() default "";
/**
* An icon representing the service. This attribute must be set as
* a reference to a drawable resource containing the image definition. If it is
* not set, the icon specified for the application as a whole is used instead.
*
* The service's icon — whether set here or by the <application>
* element — is also the default icon for all the service's intent filters
* (see the {@link IntentFilterElement#icon()} attribute).
*
* @return the service icon attribute
*/
String icon() default "";
/**
* A name for the service that can be displayed to users. If this attribute
* is not set, the label set for the application as a whole is used instead.
*
* The service's label — whether set here or by the <application> element —
* is also the default label for all the service's intent filters.
*
* @return the service label attribute
*/
String label() default "";
/**
* The name of a permission that an entity must have in order to launch the
* service or bind to it. If a caller of startService(), bindService(), or
* stopService(), has not been granted this permission, the method will not
* work and the Intent object will not be delivered to the service.
*
* If this attribute is not set, the permission set by the <application>
* element's permission attribute applies to the service. If neither attribute
* is set, the service is not protected by a permission.
*
* @return the service permission attribute
*/
String permission() default "";
/**
* The name of the process in which the service should run. Normally,
* all components of an application run in the default process created for the
* application. For our purposes, those components are services and
* activities. It has the same name as the application package. Each component
* can override the default with its own process attribute, allowing you to
* spread your application across multiple processes.
*
* If the name assigned to this attribute begins with a colon (':'), a new
* process, private to the application, is created when it's needed and the
* service runs in that process. If the process name begins with
* a lowercase character, the service will run in a global process of that
* name, provided that it has permission to do so. This allows components
* ( services and activities) in different applications to share
* a process, reducing resource usage.
*
* @return the service process attribute
*/
String process() default "";
}
......@@ -26,6 +26,8 @@ public final class ComponentDescriptorConstants {
public static final String NATIVE_TARGET = "native";
public static final String PERMISSIONS_TARGET = "permissions";
public static final String BROADCAST_RECEIVERS_TARGET = "broadcastReceivers";
public static final String SERVICES_TARGET = "services";
public static final String CONTENT_PROVIDERS_TARGET = "contentProviders";
public static final String ANDROIDMINSDK_TARGET = "androidMinSdk";
public static final String CONDITIONALS_TARGET = "conditionals";
......
......@@ -213,7 +213,9 @@ public final class ComponentDescriptorGenerator extends ComponentProcessor {
*/
private void outputConditionalAnnotations(ComponentInfo component, StringBuilder sb) {
if (component.conditionalPermissions.size() +
component.conditionalBroadcastReceivers.size() == 0) {
component.conditionalBroadcastReceivers.size() +
component.conditionalServices.size() +
component.conditionalContentProviders.size() == 0) {
return;
}
sb.append(",\n \"conditionals\":{\n ");
......@@ -229,6 +231,18 @@ public final class ComponentDescriptorGenerator extends ComponentProcessor {
outputMultimap(sb, " ", component.conditionalBroadcastReceivers);
first = false;
}
if (component.conditionalServices.size() > 0) {
if (!first) sb.append(",\n ");
sb.append("\"services\": ");
outputMultimap(sb, " ", component.conditionalServices);
first = false;
}
if (component.conditionalContentProviders.size() > 0) {
if (!first) sb.append(",\n ");
sb.append("\"contentProviders\": ");
outputMultimap(sb, " ", component.conditionalContentProviders);
first = false;
}
// Add other annotations here as needed
sb.append("\n }");
}
......
......@@ -18,8 +18,8 @@ import javax.tools.Diagnostic;
import javax.tools.FileObject;
/**
* Tool to generate a list of the simple component types, permissions, libraries, activities
* and Broadcast Receivers (build info) required for each component.
* Tool to generate a list of the simple component types, permissions, libraries, activities,
* Broadcast Receivers, Services and Content Providers (build info) required for each component.
*
* @author lizlooney@google.com (Liz Looney)
*/
......@@ -85,6 +85,8 @@ public final class ComponentListGenerator extends ComponentProcessor {
appendComponentInfo(sb, ComponentDescriptorConstants.ACTIVITY_METADATA_TARGET, component.activityMetadata);
appendComponentInfo(sb, ComponentDescriptorConstants.ANDROIDMINSDK_TARGET, Collections.singleton(Integer.toString(component.getAndroidMinSdk())));
appendComponentInfo(sb, ComponentDescriptorConstants.BROADCAST_RECEIVERS_TARGET, component.broadcastReceivers);
appendComponentInfo(sb, ComponentDescriptorConstants.SERVICES_TARGET, component.services);
appendComponentInfo(sb, ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET, component.contentProviders);
appendConditionalComponentInfo(component, sb);
// TODO(Will): Remove the following call once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
......@@ -103,7 +105,9 @@ public final class ComponentListGenerator extends ComponentProcessor {
*/
private static void appendConditionalComponentInfo(ComponentInfo component, StringBuilder sb) {
if (component.conditionalPermissions.size() +
component.conditionalBroadcastReceivers.size() == 0) {
component.conditionalBroadcastReceivers.size() +
component.conditionalServices.size() +
component.conditionalContentProviders.size() == 0) {
return;
}
sb.append(", \"" + ComponentDescriptorConstants.CONDITIONALS_TARGET + "\": { ");
......@@ -111,6 +115,10 @@ public final class ComponentListGenerator extends ComponentProcessor {
appendMap(sb, component.conditionalPermissions);
sb.append(", \"" + ComponentDescriptorConstants.BROADCAST_RECEIVERS_TARGET + "\": ");
appendMap(sb, component.conditionalBroadcastReceivers);
sb.append(", \"" + ComponentDescriptorConstants.SERVICES_TARGET + "\": ");
appendMap(sb, component.conditionalServices);
sb.append(", \"" + ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET + "\": ");
appendMap(sb, component.conditionalContentProviders);
sb.append("}");
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment