All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jayway.maven.plugins.android.AbstractAndroidMojo Maven / Gradle / Ivy

/*
 * Copyright (C) 2009-2011 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.jayway.maven.plugins.android;

import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.InstallException;
import com.jayway.maven.plugins.android.common.AetherHelper;
import com.jayway.maven.plugins.android.common.AndroidExtension;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.JXPathNotFoundException;
import org.apache.commons.jxpath.xml.DocumentContainer;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.util.DirectoryScanner;
import org.sonatype.aether.RepositorySystem;
import org.sonatype.aether.RepositorySystemSession;
import org.sonatype.aether.repository.LocalRepository;
import org.sonatype.aether.repository.RemoteRepository;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;

import static org.apache.commons.lang.StringUtils.isBlank;

/**
 * Contains common fields and methods for android mojos.
 *
 * @author [email protected]
 * @author Manfred Moser 
 */
public abstract class AbstractAndroidMojo extends AbstractMojo {

    public static final List SUPPORTED_PACKAGING_TYPES = new ArrayList();

    static {
        SUPPORTED_PACKAGING_TYPES.add(AndroidExtension.APK);
    }

    /**
     * The maven project.
     *
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    protected MavenProject project;

    /**
     * The maven session.
     *
     * @parameter expression="${session}"
     * @required
     * @readonly
     */
    protected MavenSession session;


    /**
     * The java sources directory.
     *
     * @parameter default-value="${project.build.sourceDirectory}"
     * @readonly
     */
    protected File sourceDirectory;

    /**
     * The android resources directory.
     *
     * @parameter default-value="${project.basedir}/res"
     */
    protected File resourceDirectory;

    /**
     * 

Root folder containing native libraries to include in the application package.

* * @parameter expression="${android.nativeLibrariesDirectory}" default-value="${project.basedir}/libs" */ protected File nativeLibrariesDirectory; /** * The android resources overlay directory. This will be overriden * by resourceOverlayDirectories if present. * * @parameter default-value="${project.basedir}/res-overlay" */ protected File resourceOverlayDirectory; /** * The android resources overlay directories. If this is specified, * the {@link #resourceOverlayDirectory} parameter will be ignored. * * @parameter */ protected File[] resourceOverlayDirectories; /** * The android assets directory. * * @parameter default-value="${project.basedir}/assets" */ protected File assetsDirectory; /** * The AndroidManifest.xml file. * * @parameter default-value="${project.basedir}/AndroidManifest.xml" */ protected File androidManifestFile; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies" * @readonly */ protected File extractedDependenciesDirectory; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/res" * @readonly */ protected File extractedDependenciesRes; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/assets" * @readonly */ protected File extractedDependenciesAssets; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/java" * @readonly */ protected File extractedDependenciesJavaSources; /** * @parameter expression="${project.build.directory}/generated-sources/extracted-dependencies/src/main/resources" * @readonly */ protected File extractedDependenciesJavaResources; /** * The combined resources directory. This will contain both the resources found in "res" as well as any resources contained in a apksources dependency. * * @parameter expression="${project.build.directory}/generated-sources/combined-resources/res" * @readonly */ protected File combinedRes; /** * The combined assets directory. This will contain both the assets found in "assets" as well as any assets contained in a apksources dependency. * * @parameter expression="${project.build.directory}/generated-sources/combined-assets/assets" * @readonly */ protected File combinedAssets; /** * Extract the apklib dependencies here * * @parameter expression="${project.build.directory}/unpack/apklibs" * @readonly */ protected File unpackedApkLibsDirectory; /** * Specifies which device to connect to, by serial number. Special values "usb" and "emulator" are also valid, for * selecting the only USB connected device or the only running emulator, respectively. * * @parameter expression="${android.device}" */ protected String device; /** * A selection of configurations to be included in the APK as a comma separated list. This will limit the configurations for a certain type. * For example, specifying hdpi will exclude all resource folders with the mdpi or ldpi * modifiers, but won't affect language or orientation modifiers. For more information about this option, look in the aapt command line help. * * @parameter expression="${android.configurations}" */ protected String configurations; /** * A list of extra arguments that must be passed to aapt. * * @parameter expression="${android.aaptExtraArgs}" */ protected String[] aaptExtraArgs; /** * Decides whether the Apk should be generated or not. If set to false, dx and apkBuilder will not run. This is probably most * useful for a project used to generate apk sources to be inherited into another application project. * * @parameter expression="${android.generateApk}" default-value="true" */ protected boolean generateApk; /** * The entry point to Aether, i.e. the component doing all the work. * * @component */ protected RepositorySystem repoSystem; /** * The current repository/network configuration of Maven. * * @parameter default-value="${repositorySystemSession}" * @readonly */ protected RepositorySystemSession repoSession; /** * The project's remote repositories to use for the resolution of project dependencies. * * @parameter default-value="${project.remoteProjectRepositories}" * @readonly */ protected List projectRepos; /** * Generates R.java into a different package. * * @parameter expression="${android.customPackage}" */ protected String customPackage; /** * Maven ProjectHelper. * * @component * @readonly */ protected MavenProjectHelper projectHelper; /** *

The Android SDK to use.

*

Looks like this:

*
     * <sdk>
     *     <path>/opt/android-sdk-linux</path>
     *     <platform>2.1</platform>
     * </sdk>
     * 
*

The <platform> parameter is optional, and corresponds to the * platforms/android-* directories in the Android SDK directory. Default is the latest available * version, so you only need to set it if you for example want to use platform 1.5 but also have e.g. 2.2 installed. * Has no effect when used on an Android SDK 1.1. The parameter can also be coded as the API level. Therefore valid values are * 1.1, 1.5, 1.6, 2.0, 2.01, 2.1, 2,2 as well as 3, 4, 5, 6, 7, 8. If a platform/api level is not installed on the * machine an error message will be produced.

*

The <path> parameter is optional. The default is the setting of the ANDROID_HOME environment * variable. The parameter can be used to override this setting with a different environment variable like this:

*
     * <sdk>
     *     <path>${env.ANDROID_SDK}</path>
     * </sdk>
     * 
*

or just with a hardcoded absolute path. The parameters can also be configured from command-line with parameters * -Dandroid.sdk.path and -Dandroid.sdk.platform.

* * @parameter */ private Sdk sdk; /** *

Parameter designed to pick up -Dandroid.sdk.path in case there is no pom with an * <sdk> configuration tag.

*

Corresponds to {@link Sdk#path}.

* * @parameter expression="${android.sdk.path}" * @readonly */ private File sdkPath; /** *

Parameter designed to pick up environment variable ANDROID_HOME in case * android.sdk.path is not configured.

* * @parameter expression="${env.ANDROID_HOME}" * @readonly */ private String envANDROID_HOME; /** * The ANDROID_HOME environment variable name. */ public static final String ENV_ANDROID_HOME = "ANDROID_HOME"; /** *

Parameter designed to pick up -Dandroid.sdk.platform in case there is no pom with an * <sdk> configuration tag.

*

Corresponds to {@link Sdk#platform}.

* * @parameter expression="${android.sdk.platform}" * @readonly */ private String sdkPlatform; /** *

Whether to undeploy an apk from the device before deploying it.

*

*

Only has effect when running mvn android:deploy in an Android application project manually, or * when running mvn integration-test (or mvn install) in a project with instrumentation * tests. *

*

*

It is useful to keep this set to true at all times, because if an apk with the same package was * previously signed with a different keystore, and deployed to the device, deployment will fail becuase your * keystore is different.

* * @parameter default-value=false * expression="${android.undeployBeforeDeploy}" */ protected boolean undeployBeforeDeploy; /** *

Whether to attach the normal .jar file to the build, so it can be depended on by for example integration-tests * which may then access {@code R.java} from this project.

*

Only disable it if you know you won't need it for any integration-tests. Otherwise, leave it enabled.

* * @parameter default-value=true * expression="${android.attachJar}" */ protected boolean attachJar; /** *

Whether to attach sources to the build, which can be depended on by other {@code apk} projects, for including * them in their builds.

*

Enabling this setting is only required if this project's source code and/or res(ources) will be included in * other projects, using the Maven <dependency> tag.

* * @parameter default-value=false * expression="${android.attachSources}" */ protected boolean attachSources; /** *

Whether to execute tests only in given packages

*
     * <testPackages>
     *     <testPackage>your.package.name</testPackage>
     * </testPackages>
     * 
* * @parameter */ protected List testPackages; /** *

Whether to execute test classes which are specified.

*
     * <testClasses>
     *     <testClass>your.package.name.YourTestClass</testClass>
     * </testClasses>
     * 
* * @parameter */ protected List testClasses; private static final Object adbLock = new Object(); private static boolean adbInitialized = false; /** * @return Given test classes as a comma separated string */ @SuppressWarnings("unchecked") protected String buildTestClassesString() { return buildCommaSeperatedString(testClasses); } /** * @return Given test packages as a comma separated string */ @SuppressWarnings("unchecked") protected String buildTestPackagesString() { return buildCommaSeperatedString(testPackages); } /** * Helper method to build a comma separated string from a list. * Blank strings are filtered out * * @param lines A list of strings * @return Comma separated String from given list */ protected static String buildCommaSeperatedString(List lines) { if(lines == null || lines.size() == 0) { return null; } List strings = new ArrayList(lines.size()); for(String str : lines) { // filter out blank strings if(StringUtils.isNotBlank(str)) { strings.add(StringUtils.trimToEmpty(str)); } } return StringUtils.join(strings, ","); } /** * Which dependency scopes should not be included when unpacking dependencies into the apk. */ protected static final List EXCLUDED_DEPENDENCY_SCOPES = Arrays.asList("provided", "system", "import"); /** * @return a {@code Set} of dependencies which may be extracted and otherwise included in other artifacts. Never * {@code null}. This excludes artifacts of the {@code EXCLUDED_DEPENDENCY_SCOPES} scopes. */ protected Set getRelevantCompileArtifacts() { final List allArtifacts = (List) project.getCompileArtifacts(); final Set results = filterOutIrrelevantArtifacts(allArtifacts); return results; } /** * @return a {@code Set} of direct project dependencies. Never {@code null}. This excludes artifacts of the {@code * EXCLUDED_DEPENDENCY_SCOPES} scopes. */ protected Set getRelevantDependencyArtifacts() { final Set allArtifacts = (Set) project.getDependencyArtifacts(); final Set results = filterOutIrrelevantArtifacts(allArtifacts); return results; } /** * @return a {@code List} of all project dependencies. Never {@code null}. This excludes artifacts of the {@code * EXCLUDED_DEPENDENCY_SCOPES} scopes. And * This should maintain dependency order to comply with library project resource precedence. */ protected Set getAllRelevantDependencyArtifacts() { final Set allArtifacts = (Set) project.getArtifacts(); final Set results = filterOutIrrelevantArtifacts(allArtifacts); return results; } private Set filterOutIrrelevantArtifacts(Iterable allArtifacts) { final Set results = new LinkedHashSet(); for (Artifact artifact : allArtifacts) { if (artifact == null) { continue; } if (EXCLUDED_DEPENDENCY_SCOPES.contains(artifact.getScope())) { continue; } results.add(artifact); } return results; } /** * Attempts to resolve an {@link Artifact} to a {@link File}. * * @param artifact to resolve * @return a {@link File} to the resolved artifact, never null. * @throws MojoExecutionException if the artifact could not be resolved. */ protected File resolveArtifactToFile(Artifact artifact) throws MojoExecutionException { Artifact resolvedArtifact = AetherHelper.resolveArtifact(artifact, repoSystem, repoSession, projectRepos); final File jar = resolvedArtifact.getFile(); if (jar == null) { throw new MojoExecutionException("Could not resolve artifact " + artifact.getId() + ". Please install it with \"mvn install:install-file ...\" or deploy it to a repository with \"mvn deploy:deploy-file ...\""); } return jar; } /** * Initialize the Android Debug Bridge and wait for it to start. Does not reinitialize it if it has * already been initialized (that would through and IllegalStateException...). Synchronized sine * the init call in the library is also synchronized .. just in case. * @return */ private AndroidDebugBridge initAndroidDebugBridge() throws MojoExecutionException { synchronized (adbLock) { if (!adbInitialized) { AndroidDebugBridge.init(false); adbInitialized = true; } AndroidDebugBridge androidDebugBridge = AndroidDebugBridge.createBridge(getAndroidSdk().getAdbPath(), false); waitUntilConnected(androidDebugBridge); return androidDebugBridge; } } /** * Run a wait loop until adb is connected or trials run out. This method seems to work more reliably then using a * listener. * @param adb */ private void waitUntilConnected(AndroidDebugBridge adb) { int trials = 10; while (trials > 0) { try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } if (adb.isConnected()) { break; } trials--; } } /** * Deploys an apk file to a connected emulator or usb device. * * @param apkFile the file to deploy * @throws MojoExecutionException If there is a problem deploying the apk file. */ protected void deployApk(final File apkFile) throws MojoExecutionException, MojoFailureException { if (undeployBeforeDeploy) { undeployApk(apkFile); } doWithDevices(new DeviceCallback(){ public void doWithDevice(final IDevice device) throws MojoExecutionException { try { device.installPackage(apkFile.getAbsolutePath(), true); getLog().info("Successfully installed " + apkFile.getAbsolutePath() + " to " + device.getSerialNumber() + " (avdName=" + device.getAvdName() + ")"); } catch (InstallException e) { throw new MojoExecutionException("Install of " + apkFile.getAbsolutePath() + "failed.", e); } } }); } /** * Determines which {@link IDevice}(s) to use, and performs the callback action on it/them. * * @param deviceCallback the action to perform on each device * @throws org.apache.maven.plugin.MojoExecutionException in case there is a problem * @throws org.apache.maven.plugin.MojoFailureException in case there is a problem */ protected void doWithDevices(final DeviceCallback deviceCallback) throws MojoExecutionException, MojoFailureException { final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge(); if (androidDebugBridge.isConnected()) { List devices = Arrays.asList(androidDebugBridge.getDevices()); int numberOfDevices = devices.size(); getLog().info("Found " + numberOfDevices + " devices connected with the Android Debug Bridge"); if (devices.size() > 0) { if (StringUtils.isNotBlank(device)) { getLog().info("android.device parameter set to " + device); for (IDevice idevice : devices) { // use specified device or all emulators or all devices if (("emulator".equals(device) && idevice.isEmulator()) || ("usb".equals(device) && !idevice.isEmulator()) || (idevice.getAvdName() != null && idevice.getAvdName().equals(device))) { deviceCallback.doWithDevice(idevice); } } } else { getLog().info("android.device parameter not set, using all attached devices"); for (IDevice idevice : devices) { deviceCallback.doWithDevice(idevice); } } } else { throw new MojoExecutionException("No online devices attached."); } } else { throw new MojoExecutionException("Android Debug Bridge is not connected."); } } /** * Adds relevant parameter to the parameters list for chosen device. * * @param commands the parameters to be used with the {@code adb} command * @param device the device to be used */ protected void addDeviceParameter(List commands, IDevice device) { commands.add("-s"); commands.add(device.getSerialNumber()); } /** * Undeploys an apk from a connected emulator or usb device. Also deletes the application's data and cache * directories on the device. * * @param apkFile the file to undeploy * @return true if successfully undeployed, false otherwise. */ protected boolean undeployApk(File apkFile) throws MojoExecutionException, MojoFailureException { final String packageName; packageName = extractPackageNameFromApk(apkFile); return undeployApk(packageName); } /** * Undeploys an apk, specified by package name, from a connected emulator * or usb device. Also deletes the application's data and cache * directories on the device. * * @param packageName the package name to undeploy. * @return true if successfully undeployed, false otherwise. */ protected boolean undeployApk(final String packageName) throws MojoExecutionException, MojoFailureException { final AtomicBoolean result = new AtomicBoolean(true); // if no devices are present, it counts as successful doWithDevices(new DeviceCallback() { public void doWithDevice(final IDevice device) throws MojoExecutionException { try { device.uninstallPackage(packageName); getLog().info("Successfully uninstalled " + packageName + " from " + device.getSerialNumber() + " (avdName=" + device.getAvdName() + ")"); result.set(true); } catch (InstallException e) { result.set(false); throw new MojoExecutionException("Uninstall of " + packageName + "failed.", e); } } }); return result.get(); } /** * Extracts the package name from an apk file. * * @param apkFile apk file to extract package name from. * @return the package name from inside the apk file. */ protected String extractPackageNameFromApk(File apkFile) throws MojoExecutionException { CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor(); executor.setLogger(this.getLog()); List commands = new ArrayList(); commands.add("dump"); commands.add("xmltree"); commands.add(apkFile.getAbsolutePath()); commands.add("AndroidManifest.xml"); getLog().info(getAndroidSdk().getPathForTool("aapt") + " " + commands.toString()); try { executor.executeCommand(getAndroidSdk().getPathForTool("aapt"), commands, false); final String xmlTree = executor.getStandardOut(); return extractPackageNameFromAndroidManifestXmlTree(xmlTree); } catch (ExecutionException e) { throw new MojoExecutionException("Error while trying to figure out package name from inside apk file " + apkFile); } finally { String errout = executor.getStandardError(); if ((errout != null) && (errout.trim().length() > 0)) { getLog().error(errout); } } } /** * Extracts the package name from an XmlTree dump of AndroidManifest.xml by the aapt tool. * * @param aaptDumpXmlTree output from aapt dump xmltree <apkFile> AndroidManifest.xml * @return the package name from inside the apkFile. */ protected String extractPackageNameFromAndroidManifestXmlTree(String aaptDumpXmlTree) { final Scanner scanner = new Scanner(aaptDumpXmlTree); // Finds the root element named "manifest". scanner.findWithinHorizon("^E: manifest", 0); // Finds the manifest element's attribute named "package". scanner.findWithinHorizon(" A: package=\"", 0); // Extracts the package value including the trailing double quote. String packageName = scanner.next(".*?\""); // Removes the double quote. packageName = packageName.substring(0, packageName.length() - 1); return packageName; } protected String extractPackageNameFromAndroidManifest(File androidManifestFile) throws MojoExecutionException { final URL xmlURL; try { xmlURL = androidManifestFile.toURI().toURL(); } catch (MalformedURLException e) { throw new MojoExecutionException("Error while trying to figure out package name from inside AndroidManifest.xml file " + androidManifestFile, e); } final DocumentContainer documentContainer = new DocumentContainer(xmlURL); final Object packageName = JXPathContext.newContext(documentContainer).getValue("manifest/@package", String.class); return (String) packageName; } /** * Attempts to find the instrumentation test runner from inside the AndroidManifest.xml file. * * @param androidManifestFile the AndroidManifest.xml file to inspect. * @return the instrumentation test runner declared in AndroidManifest.xml, or {@code null} if it is not declared. * @throws MojoExecutionException */ protected String extractInstrumentationRunnerFromAndroidManifest(File androidManifestFile) throws MojoExecutionException { final URL xmlURL; try { xmlURL = androidManifestFile.toURI().toURL(); } catch (MalformedURLException e) { throw new MojoExecutionException("Error while trying to figure out instrumentation runner from inside AndroidManifest.xml file " + androidManifestFile, e); } final DocumentContainer documentContainer = new DocumentContainer(xmlURL); final Object instrumentationRunner; try { instrumentationRunner = JXPathContext.newContext(documentContainer).getValue("manifest//instrumentation/@android:name", String.class); } catch (JXPathNotFoundException e) { return null; } return (String) instrumentationRunner; } protected int deleteFilesFromDirectory(File baseDirectory, String... includes) throws MojoExecutionException { final String[] files = findFilesInDirectory(baseDirectory, includes); if (files == null) { return 0; } for (String file : files) { final boolean successfullyDeleted = new File(baseDirectory, file).delete(); if (!successfullyDeleted) { throw new MojoExecutionException("Failed to delete \"" + file + "\""); } } return files.length; } /** * Finds files. * * @param baseDirectory Directory to find files in. * @param includes Ant-style include statements, for example "** /*.aidl" (but without the space in the middle) * @return String[] of the files' paths and names, relative to baseDirectory. Empty String[] if baseDirectory does not exist. */ protected String[] findFilesInDirectory(File baseDirectory, String... includes) { if (baseDirectory.exists()) { DirectoryScanner directoryScanner = new DirectoryScanner(); directoryScanner.setBasedir(baseDirectory); directoryScanner.setIncludes(includes); directoryScanner.addDefaultExcludes(); directoryScanner.scan(); String[] files = directoryScanner.getIncludedFiles(); return files; } else { return new String[0]; } } /** *

Returns the Android SDK to use.

*

*

Current implementation looks for <sdk><path> configuration in pom, then System * property android.sdk.path, then environment variable ANDROID_HOME. *

*

This is where we collect all logic for how to lookup where it is, and which one to choose. The lookup is * based on available parameters. This method should be the only one you should need to look at to understand how * the Android SDK is chosen, and from where on disk.

* * @return the Android SDK to use. * @throws org.apache.maven.plugin.MojoExecutionException * if no Android SDK path configuration is available at all. */ protected AndroidSdk getAndroidSdk() throws MojoExecutionException { File chosenSdkPath; String chosenSdkPlatform; if (sdk != null) { // An tag exists in the pom. if (sdk.getPath() != null) { // An tag is set in the pom. chosenSdkPath = sdk.getPath(); } else { // There is no tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = new File(getAndroidHomeOrThrow()); } } // Use from pom if it's there, otherwise try -Dandroid.sdk.platform from command line or ... if (!isBlank(sdk.getPlatform())) { chosenSdkPlatform = sdk.getPlatform(); } else { chosenSdkPlatform = sdkPlatform; } } else { // There is no tag in the pom. if (sdkPath != null) { // -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = sdkPath; } else { // No -Dandroid.sdk.path is set on command line, or via ... chosenSdkPath = new File(getAndroidHomeOrThrow()); } // Use any -Dandroid.sdk.platform from command line or ... chosenSdkPlatform = sdkPlatform; } return new AndroidSdk(chosenSdkPath, chosenSdkPlatform); } private String getAndroidHomeOrThrow() throws MojoExecutionException { final String androidHome = System.getenv(ENV_ANDROID_HOME); if (isBlank(androidHome)) { throw new MojoExecutionException("No Android SDK path could be found. You may configure it in the pom using ... or ... or on command-line using -Dandroid.sdk.path=... or by setting environment variable " + ENV_ANDROID_HOME); } return androidHome; } protected String getLibraryUnpackDirectory(Artifact apkLibraryArtifact) { return unpackedApkLibsDirectory.getAbsolutePath() + "/" + apkLibraryArtifact.getId().replace(":", "_"); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy