Merge commit '1af55876a53bb28daa8b7533e0c934875b695f2f' into HEAD
diff --git a/common/src/main/java/com/android/SdkConstants.java b/common/src/main/java/com/android/SdkConstants.java
index 2138dcb..37510ce 100644
--- a/common/src/main/java/com/android/SdkConstants.java
+++ b/common/src/main/java/com/android/SdkConstants.java
@@ -203,6 +203,7 @@
* filename for gdbserver.
*/
public static final String FN_GDBSERVER = "gdbserver"; //$NON-NLS-1$
+ public static final String FN_GDB_SETUP = "gdb.setup"; //$NON-NLS-1$
/** global Android proguard config file */
public static final String FN_ANDROID_PROGUARD_FILE = "proguard-android.txt"; //$NON-NLS-1$
diff --git a/common/src/main/java/com/android/utils/HtmlBuilder.java b/common/src/main/java/com/android/utils/HtmlBuilder.java
index 3c3159e..f2e0d20 100644
--- a/common/src/main/java/com/android/utils/HtmlBuilder.java
+++ b/common/src/main/java/com/android/utils/HtmlBuilder.java
@@ -70,6 +70,14 @@
return this;
}
+ public HtmlBuilder newlineIfNecessary() {
+ if (!SdkUtils.endsWith(mStringBuilder, "<BR/>\n")) {
+ mStringBuilder.append("<BR/>\n");
+ }
+
+ return this;
+ }
+
public HtmlBuilder addLink(@Nullable String textBefore,
@NonNull String linkText,
@Nullable String textAfter,
diff --git a/common/src/test/java/com/android/utils/HtmlBuilderTest.java b/common/src/test/java/com/android/utils/HtmlBuilderTest.java
index ff54db3..ab80bdd 100644
--- a/common/src/test/java/com/android/utils/HtmlBuilderTest.java
+++ b/common/src/test/java/com/android/utils/HtmlBuilderTest.java
@@ -122,4 +122,20 @@
}
assertEquals(expected, actual);
}
+
+ public void testNewlineIfNecessary() {
+ HtmlBuilder builder = new HtmlBuilder();
+ builder.newlineIfNecessary();
+ assertEquals("<BR/>\n", builder.getHtml());
+ builder.newlineIfNecessary();
+ assertEquals("<BR/>\n", builder.getHtml());
+ builder.add("a");
+ builder.newlineIfNecessary();
+ assertEquals("<BR/>\na<BR/>\n", builder.getHtml());
+ builder.newline();
+ builder.newlineIfNecessary();
+ builder.newlineIfNecessary();
+ builder.newlineIfNecessary();
+ assertEquals("<BR/>\na<BR/>\n<BR/>\n", builder.getHtml());
+ }
}
diff --git a/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java b/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java
index e262cf3..efe092c 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/CollectingOutputReceiver.java
@@ -18,6 +18,7 @@
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* A {@link IShellOutputReceiver} which collects the whole shell output into one
@@ -26,7 +27,7 @@
public class CollectingOutputReceiver implements IShellOutputReceiver {
private CountDownLatch mCompletionLatch;
private StringBuffer mOutputBuffer = new StringBuffer();
- private boolean mIsCanceled = false;
+ private AtomicBoolean mIsCanceled = new AtomicBoolean(false);
public CollectingOutputReceiver() {
}
@@ -41,20 +42,20 @@
@Override
public boolean isCancelled() {
- return mIsCanceled;
+ return mIsCanceled.get();
}
/**
* Cancel the output collection
*/
public void cancel() {
- mIsCanceled = true;
+ mIsCanceled.set(true);
}
@Override
public void addOutput(byte[] data, int offset, int length) {
if (!isCancelled()) {
- String s = null;
+ String s;
try {
s = new String(data, offset, length, "UTF-8"); //$NON-NLS-1$
} catch (UnsupportedEncodingException e) {
diff --git a/ddmlib/src/main/java/com/android/ddmlib/Device.java b/ddmlib/src/main/java/com/android/ddmlib/Device.java
index 0bf7cb3..8e9bece 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/Device.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/Device.java
@@ -16,6 +16,9 @@
package com.android.ddmlib;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
import com.android.annotations.concurrency.GuardedBy;
import com.android.ddmlib.log.LogReceiver;
@@ -38,7 +41,6 @@
* A Device. It can be a physical device or an emulator.
*/
final class Device implements IDevice {
-
private static final int INSTALL_TIMEOUT = 2*60*1000; //2min
private static final int BATTERY_TIMEOUT = 2*1000; //2 seconds
private static final int GETPROP_TIMEOUT = 2*1000; //2 seconds
@@ -81,6 +83,13 @@
private Integer mLastBatteryLevel = null;
private long mLastBatteryCheckTime = 0;
+ /** Path to the screen recorder binary on the device. */
+ private static final String SCREEN_RECORDER_DEVICE_PATH = "/system/bin/screenrecord";
+
+ /** Flag indicating whether the device has the screen recorder binary. */
+ private Boolean mHasScreenRecorder;
+
+ private int mApiLevel;
private String mName;
/**
@@ -175,6 +184,50 @@
}
}
+ /**
+ * Output receiver for "cat /sys/class/power_supply/.../capacity" command line.
+ */
+ static final class SysFsBatteryLevelReceiver extends MultiLineReceiver {
+
+ private static final Pattern BATTERY_LEVEL = Pattern.compile("^(\\d+)[.\\s]*");
+ private Integer mBatteryLevel = null;
+
+ /**
+ * Get the parsed battery level.
+ * @return battery level or <code>null</code> if it cannot be determined
+ */
+ @Nullable
+ public Integer getBatteryLevel() {
+ return mBatteryLevel;
+ }
+
+ @Override
+ public boolean isCancelled() {
+ return false;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ Matcher batteryMatch = BATTERY_LEVEL.matcher(line);
+ if (batteryMatch.matches()) {
+ if (mBatteryLevel == null) {
+ mBatteryLevel = Integer.parseInt(batteryMatch.group(1));
+ } else {
+ // multiple matches, check if they are different
+ Integer tmpLevel = Integer.parseInt(batteryMatch.group(1));
+ if (!mBatteryLevel.equals(tmpLevel)) {
+ Log.w(LOG_TAG, String.format(
+ "Multiple lines matched with different value; " +
+ "Original: %s, Current: %s (keeping original)",
+ mBatteryLevel.toString(), tmpLevel.toString()));
+ }
+ }
+ }
+ }
+ }
+ }
+
/*
* (non-Javadoc)
* @see com.android.ddmlib.IDevice#getSerialNumber()
@@ -354,6 +407,56 @@
}
@Override
+ public boolean supportsFeature(Feature feature) {
+ switch (feature) {
+ case SCREEN_RECORD:
+ if (getApiLevel() < 19) {
+ return false;
+ }
+ if (mHasScreenRecorder == null) {
+ mHasScreenRecorder = hasBinary(SCREEN_RECORDER_DEVICE_PATH);
+ }
+ return mHasScreenRecorder;
+ case PROCSTATS:
+ return getApiLevel() >= 19;
+ default:
+ return false;
+ }
+ }
+
+ private int getApiLevel() {
+ if (mApiLevel > 0) {
+ return mApiLevel;
+ }
+
+ try {
+ mApiLevel = Integer.parseInt(getPropertyCacheOrSync(PROP_BUILD_API_LEVEL));
+ return mApiLevel;
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ private boolean hasBinary(String path) {
+ CountDownLatch latch = new CountDownLatch(1);
+ CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch);
+ try {
+ executeShellCommand("ls " + path, receiver);
+ } catch (Exception e) {
+ return false;
+ }
+
+ try {
+ latch.await(GETPROP_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ return false;
+ }
+
+ String value = receiver.getOutput().trim();
+ return !value.endsWith("No such file or directory");
+ }
+
+ @Override
public String getMountPoint(String name) {
return mMountPoints.get(name);
}
@@ -431,6 +534,50 @@
}
@Override
+ public void startScreenRecorder(String remoteFilePath, ScreenRecorderOptions options,
+ IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException,
+ IOException, ShellCommandUnresponsiveException {
+ executeShellCommand(getScreenRecorderCommand(remoteFilePath, options), receiver, 0, null);
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ static String getScreenRecorderCommand(@NonNull String remoteFilePath,
+ @NonNull ScreenRecorderOptions options) {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("screenrecord");
+ sb.append(' ');
+
+ if (options.width > 0 && options.height > 0) {
+ sb.append("--size ");
+ sb.append(options.width);
+ sb.append('x');
+ sb.append(options.height);
+ sb.append(' ');
+ }
+
+ if (options.bitrateMbps > 0) {
+ sb.append("--bit-rate ");
+ sb.append(options.bitrateMbps * 1000000);
+ sb.append(' ');
+ }
+
+ if (options.timeLimit > 0) {
+ sb.append("--time-limit ");
+ long seconds = TimeUnit.SECONDS.convert(options.timeLimit, options.timeLimitUnits);
+ if (seconds > 180) {
+ seconds = 180;
+ }
+ sb.append(seconds);
+ sb.append(' ');
+ }
+
+ sb.append(remoteFilePath);
+
+ return sb.toString();
+ }
+
+ @Override
public void executeShellCommand(String command, IShellOutputReceiver receiver)
throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException,
IOException {
@@ -874,6 +1021,16 @@
&& mLastBatteryCheckTime > (System.currentTimeMillis() - freshnessMs)) {
return mLastBatteryLevel;
}
+ // first try to get it from sysfs
+ SysFsBatteryLevelReceiver sysBattReceiver = new SysFsBatteryLevelReceiver();
+ executeShellCommand("cat /sys/class/power_supply/*/capacity",
+ sysBattReceiver, BATTERY_TIMEOUT);
+ mLastBatteryLevel = sysBattReceiver.getBatteryLevel();
+ if (mLastBatteryLevel != null) {
+ mLastBatteryCheckTime = System.currentTimeMillis();
+ return mLastBatteryLevel;
+ }
+ // now try dumpsys
BatteryReceiver receiver = new BatteryReceiver();
executeShellCommand("dumpsys battery", receiver, BATTERY_TIMEOUT);
mLastBatteryLevel = receiver.getBatteryLevel();
diff --git a/ddmlib/src/main/java/com/android/ddmlib/IDevice.java b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
index 7b70acb..c8d72ae 100644
--- a/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
+++ b/ddmlib/src/main/java/com/android/ddmlib/IDevice.java
@@ -16,6 +16,7 @@
package com.android.ddmlib;
+import com.android.annotations.NonNull;
import com.android.ddmlib.log.LogReceiver;
import java.io.IOException;
@@ -31,6 +32,8 @@
public static final String PROP_BUILD_CODENAME = "ro.build.version.codename";
public static final String PROP_DEVICE_MODEL = "ro.product.model";
public static final String PROP_DEVICE_MANUFACTURER = "ro.product.manufacturer";
+ public static final String PROP_DEVICE_CPU_ABI = "ro.product.cpu.abi";
+ public static final String PROP_DEVICE_CPU_ABI2 = "ro.product.cpu.abi2";
public static final String PROP_DEBUGGABLE = "ro.debuggable";
@@ -43,6 +46,12 @@
/** Device change bit mask: build info change. */
public static final int CHANGE_BUILD_INFO = 0x0004;
+ /** List of device level features. */
+ public enum Feature {
+ SCREEN_RECORD, // screen recorder available?
+ PROCSTATS, // procstats service (dumpsys procstats) available
+ };
+
/** @deprecated Use {@link #PROP_BUILD_API_LEVEL}. */
@Deprecated
public static final String PROP_BUILD_VERSION_NUMBER = PROP_BUILD_API_LEVEL;
@@ -176,6 +185,9 @@
public String getPropertyCacheOrSync(String name) throws TimeoutException,
AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException;
+ /** Returns whether this device supports the given feature. */
+ boolean supportsFeature(@NonNull Feature feature);
+
/**
* Returns a mount point.
*
@@ -262,6 +274,14 @@
IOException;
/**
+ * Initiates screen recording on the device if the device supports {@link Feature#SCREEN_RECORD}.
+ */
+ public void startScreenRecorder(@NonNull String remoteFilePath,
+ @NonNull ScreenRecorderOptions options, @NonNull IShellOutputReceiver receiver) throws
+ TimeoutException, AdbCommandRejectedException, IOException,
+ ShellCommandUnresponsiveException;
+
+ /**
* @deprecated Use {@link #executeShellCommand(String, IShellOutputReceiver, long, java.util.concurrent.TimeUnit)}.
*/
@Deprecated
diff --git a/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java b/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java
new file mode 100644
index 0000000..af8952a
--- /dev/null
+++ b/ddmlib/src/main/java/com/android/ddmlib/ScreenRecorderOptions.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ddmlib;
+
+import java.util.concurrent.TimeUnit;
+
+public class ScreenRecorderOptions {
+ // video size is given by width x height, defaults to device's main display resolution
+ // or 1280x720.
+ public final int width;
+ public final int height;
+
+ // bit rate in Mbps. Defaults to 4Mbps
+ public final int bitrateMbps;
+
+ // time limit, maximum of 3 seconds
+ public final long timeLimit;
+ public final TimeUnit timeLimitUnits;
+
+ private ScreenRecorderOptions(Builder builder) {
+ width = builder.mWidth;
+ height = builder.mHeight;
+
+ bitrateMbps = builder.mBitRate;
+
+ timeLimit = builder.mTime;
+ timeLimitUnits = builder.mTimeUnits;
+ }
+
+ public static class Builder {
+ private int mWidth;
+ private int mHeight;
+ private int mBitRate;
+ private long mTime;
+ private TimeUnit mTimeUnits;
+
+ public Builder setSize(int w, int h) {
+ mWidth = w;
+ mHeight = h;
+ return this;
+ }
+
+ public Builder setBitRate(int bitRateMbps) {
+ mBitRate = bitRateMbps;
+ return this;
+ }
+
+ public Builder setTimeLimit(long time, TimeUnit units) {
+ mTime = time;
+ mTimeUnits = units;
+ return this;
+ }
+
+ public ScreenRecorderOptions build() {
+ return new ScreenRecorderOptions(this);
+ }
+ }
+}
diff --git a/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java b/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java
new file mode 100644
index 0000000..fb55987
--- /dev/null
+++ b/ddmlib/src/test/java/com/android/ddmlib/DeviceTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ddmlib;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.TimeUnit;
+
+public class DeviceTest extends TestCase {
+ public void testScreenRecorderOptions() {
+ ScreenRecorderOptions options =
+ new ScreenRecorderOptions.Builder()
+ .setBitRate(6)
+ .setSize(600,400)
+ .build();
+ assertEquals("screenrecord --size 600x400 --bit-rate 6000000 /sdcard/1.mp4",
+ Device.getScreenRecorderCommand("/sdcard/1.mp4", options));
+
+ options = new ScreenRecorderOptions.Builder().setTimeLimit(100, TimeUnit.SECONDS).build();
+ assertEquals("screenrecord --time-limit 100 /sdcard/1.mp4",
+ Device.getScreenRecorderCommand("/sdcard/1.mp4", options));
+
+ options = new ScreenRecorderOptions.Builder().setTimeLimit(4, TimeUnit.MINUTES).build();
+ assertEquals("screenrecord --time-limit 180 /sdcard/1.mp4",
+ Device.getScreenRecorderCommand("/sdcard/1.mp4", options));
+ }
+}
diff --git a/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java b/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java
new file mode 100644
index 0000000..70970c3
--- /dev/null
+++ b/ddmlib/src/test/java/com/android/ddmlib/SysFsBatteryLevelReceiverTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ddmlib;
+
+import com.android.ddmlib.Device.SysFsBatteryLevelReceiver;
+
+import junit.framework.TestCase;
+
+import java.util.Random;
+
+public class SysFsBatteryLevelReceiverTest extends TestCase {
+
+ private SysFsBatteryLevelReceiver mReceiver;
+ private Integer mExpected1, mExpected2;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mReceiver = new SysFsBatteryLevelReceiver();
+ Random r = new Random(System.currentTimeMillis());
+ mExpected1 = r.nextInt(101);
+ mExpected2 = r.nextInt(101);
+ }
+
+ public void testSingleLine() {
+ String[] lines = {mExpected1.toString()};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testWithTrailingWhitespace1() {
+ String[] lines = {mExpected1 + " "};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testWithTrailingWhitespace2() {
+ String[] lines = {mExpected1 + "\n"};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testWithTrailingWhitespace3() {
+ String[] lines = {mExpected1 + "\r"};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testWithTrailingWhitespace4() {
+ String[] lines = {mExpected1 + "\r\n"};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testMultipleLinesSame() {
+ String[] lines = {mExpected1 + "\n", mExpected2.toString()};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testMultipleLinesDifferent() {
+ String[] lines = {mExpected1 + "\n", mExpected2.toString()};
+ mReceiver.processNewLines(lines);
+ assertEquals(mExpected1, mReceiver.getBatteryLevel());
+ }
+
+ public void testInvalid() {
+ String[] lines = {"foo\n", "bar", "yadda"};
+ mReceiver.processNewLines(lines);
+ assertNull(mReceiver.getBatteryLevel());
+ }
+}
diff --git a/device_validator/dvlib/src/main/resources/com/android/dvlib/devices.xsd b/device_validator/dvlib/src/main/resources/com/android/dvlib/devices.xsd
index abb1443..34d197a 100644
--- a/device_validator/dvlib/src/main/resources/com/android/dvlib/devices.xsd
+++ b/device_validator/dvlib/src/main/resources/com/android/dvlib/devices.xsd
@@ -465,6 +465,7 @@
<xsd:enumeration value="hdpi" />
<xsd:enumeration value="xhdpi" />
<xsd:enumeration value="xxhdpi" />
+ <xsd:enumeration value="xxxhdpi" />
</xsd:restriction>
</xsd:simpleType>
</xsd:element>
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b0e28e1..71afaf0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=../../../external/gradle/gradle-1.7-bin.zip
+distributionUrl=../../../external/gradle/gradle-1.9-bin.zip
diff --git a/layoutlib-api/src/main/java/com/android/resources/FolderTypeRelationship.java b/layoutlib-api/src/main/java/com/android/resources/FolderTypeRelationship.java
index d174940..a951056 100644
--- a/layoutlib-api/src/main/java/com/android/resources/FolderTypeRelationship.java
+++ b/layoutlib-api/src/main/java/com/android/resources/FolderTypeRelationship.java
@@ -63,6 +63,7 @@
add(ResourceType.STRING, ResourceFolderType.VALUES);
add(ResourceType.STYLE, ResourceFolderType.VALUES);
add(ResourceType.STYLEABLE, ResourceFolderType.VALUES);
+ add(ResourceType.TRANSITION, ResourceFolderType.TRANSITION);
add(ResourceType.XML, ResourceFolderType.XML);
makeSafe();
diff --git a/layoutlib-api/src/main/java/com/android/resources/ResourceConstants.java b/layoutlib-api/src/main/java/com/android/resources/ResourceConstants.java
index 4e92e3c..7d73f70 100644
--- a/layoutlib-api/src/main/java/com/android/resources/ResourceConstants.java
+++ b/layoutlib-api/src/main/java/com/android/resources/ResourceConstants.java
@@ -43,6 +43,8 @@
public static final String FD_RES_XML = "xml"; //$NON-NLS-1$
/** Default raw resource folder name, i.e. "raw" */
public static final String FD_RES_RAW = "raw"; //$NON-NLS-1$
+ /** Default transition resource folder name, i.e. "transition" */
+ public static final String FD_RES_TRANSITION = "transition"; //$NON-NLS-1$
/** Separator between the resource folder qualifier. */
public static final String RES_QUALIFIER_SEP = "-"; //$NON-NLS-1$
diff --git a/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java b/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java
index 5a271cf..0e78697 100644
--- a/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java
+++ b/layoutlib-api/src/main/java/com/android/resources/ResourceFolderType.java
@@ -29,6 +29,7 @@
MENU(ResourceConstants.FD_RES_MENU),
MIPMAP(ResourceConstants.FD_RES_MIPMAP),
RAW(ResourceConstants.FD_RES_RAW),
+ TRANSITION(ResourceConstants.FD_RES_TRANSITION),
VALUES(ResourceConstants.FD_RES_VALUES),
XML(ResourceConstants.FD_RES_XML);
diff --git a/layoutlib-api/src/main/java/com/android/resources/ResourceType.java b/layoutlib-api/src/main/java/com/android/resources/ResourceType.java
index f02152a..891c65b 100644
--- a/layoutlib-api/src/main/java/com/android/resources/ResourceType.java
+++ b/layoutlib-api/src/main/java/com/android/resources/ResourceType.java
@@ -42,6 +42,7 @@
STRING("string", "String"), //$NON-NLS-1$
STYLE("style", "Style"), //$NON-NLS-1$
STYLEABLE("styleable", "Styleable"), //$NON-NLS-1$
+ TRANSITION("transition", "Transition"), //$NON-NLS-1$
XML("xml", "XML"), //$NON-NLS-1$
// this is not actually used. Only there because they get parsed and since we want to
// detect new resource type, we need to have this one exist.
diff --git a/legacy/ant-tasks/src/main/java/com/android/ant/ComputeDependencyTask.java b/legacy/ant-tasks/src/main/java/com/android/ant/ComputeDependencyTask.java
index 33a1fa7..7b974d4 100644
--- a/legacy/ant-tasks/src/main/java/com/android/ant/ComputeDependencyTask.java
+++ b/legacy/ant-tasks/src/main/java/com/android/ant/ComputeDependencyTask.java
@@ -142,7 +142,7 @@
throw new BuildException("Missing attribute buildToolsFolder");
}
if (mRenderscriptSupportLibsOut == null) {
- throw new BuildException("Missing attribute renderscriptSupportOutOut");
+ throw new BuildException("Missing attribute renderscriptSupportLibsOut");
}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/CheckPermissionDetectorTest.java b/lint/cli/src/test/java/com/android/tools/lint/checks/CheckPermissionDetectorTest.java
new file mode 100644
index 0000000..caca6a5
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/CheckPermissionDetectorTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.tools.lint.checks;
+
+import com.android.tools.lint.detector.api.Detector;
+
+public class CheckPermissionDetectorTest extends AbstractCheckTest {
+ @Override
+ protected Detector getDetector() {
+ return new CheckPermissionDetector();
+ }
+
+ public void test() throws Exception {
+ assertEquals(""
+ + "src/test/pkg/CheckPermissions.java:7: Warning: The result of checkCallingOrSelfPermission is not used; did you mean to call enforceCallingOrSelfPermission? [UseCheckPermission]\n"
+ + " context.checkCallingOrSelfPermission(Manifest.permission.INTERNET); // WRONG\n"
+ + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ + "0 errors, 1 warnings\n",
+
+ lintProject("src/test/pkg/CheckPermissions.java.txt=>" +
+ "src/test/pkg/CheckPermissions.java"));
+ }
+}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/SecureRandomGeneratorDetectorTest.java b/lint/cli/src/test/java/com/android/tools/lint/checks/SecureRandomGeneratorDetectorTest.java
new file mode 100644
index 0000000..3b7e1fd
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/SecureRandomGeneratorDetectorTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.tools.lint.checks;
+
+import com.android.tools.lint.detector.api.Detector;
+
+@SuppressWarnings("SpellCheckingInspection")
+public class SecureRandomGeneratorDetectorTest extends AbstractCheckTest {
+
+ @Override
+ protected Detector getDetector() {
+ return new SecureRandomGeneratorDetector();
+ }
+
+ public void testWithoutWorkaround() throws Exception {
+ assertEquals(""
+ + "src/test/pkg/PrngCalls.java:13: Warning: Potentially insecure random numbers "
+ + "on Android 4.3 and older. Read "
+ + "https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html"
+ + " for more info. [TrulyRandom]\n"
+ + " KeyGenerator generator = KeyGenerator.getInstance(\"AES\", \"BC\");\n"
+ + " ~~~~~~~~~~~\n"
+ + "0 errors, 1 warnings\n",
+ lintProject(
+ "bytecode/.classpath=>.classpath",
+ "bytecode/AndroidManifest.xml=>AndroidManifest.xml",
+ "bytecode/PrngCalls.java.txt=>src/test/pkg/PrngCalls.java",
+ "bytecode/PrngCalls.class.data=>bin/classes/test/pkg/PrngCalls.class"
+ ));
+ }
+
+ public void testWithWorkaround() throws Exception {
+ assertEquals(
+ "No warnings.",
+ lintProject(
+ "bytecode/.classpath=>.classpath",
+ "bytecode/AndroidManifest.xml=>AndroidManifest.xml",
+ "bytecode/PrngCalls.java.txt=>src/test/pkg/PrngCalls.java",
+ "bytecode/PrngCalls.class.data=>bin/classes/test/pkg/PrngCalls.class",
+ "bytecode/PrngWorkaround$LinuxPRNGSecureRandom.class.data=>bin/classes/test/pkg/PrngWorkaround$LinuxPRNGSecureRandom.class",
+ "bytecode/PrngWorkaround$LinuxPRNGSecureRandomProvider.class.data=>bin/classes/test/pkg/PrngWorkaround$LinuxPRNGSecureRandomProvider.class",
+ "bytecode/PrngWorkaround.class.data=>bin/classes/test/pkg/PrngWorkaround.class"
+ ));
+ }
+
+ public void testCipherInit() throws Exception {
+ assertEquals(""
+ + "src/test/pkg/CipherTest1.java:11: Warning: Potentially insecure random "
+ + "numbers on Android 4.3 and older. Read "
+ + "https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html"
+ + " for more info. [TrulyRandom]\n"
+ + " cipher.init(Cipher.WRAP_MODE, key); // FLAG\n"
+ + " ~~~~\n"
+ + "0 errors, 1 warnings\n",
+ lintProject(
+ "bytecode/.classpath=>.classpath",
+ "bytecode/AndroidManifest.xml=>AndroidManifest.xml",
+ "bytecode/CipherTest1.java.txt=>src/test/pkg/CipherTest1.java",
+ "bytecode/CipherTest1.class.data=>bin/classes/test/pkg/CipherTest1.class"
+ ));
+ }
+
+ public void testGetArity() {
+ assertEquals(2, SecureRandomGeneratorDetector.getDescArity("(ILjava/security/Key;)V"));
+ assertEquals(0, SecureRandomGeneratorDetector.getDescArity("()V"));
+ assertEquals(1, SecureRandomGeneratorDetector.getDescArity("(I)V"));
+ assertEquals(3, SecureRandomGeneratorDetector.getDescArity(
+ "(Ljava/lang/String;Ljava/lang/String;I)V"));
+ assertEquals(0, SecureRandomGeneratorDetector.getDescArity("()Lfoo/bar/Baz;"));
+ }
+}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.class.data
new file mode 100644
index 0000000..73cd72f
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.java.txt
new file mode 100644
index 0000000..200de79
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/CipherTest1.java.txt
@@ -0,0 +1,21 @@
+package test.pkg;
+
+import java.security.Key;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+
+@SuppressWarnings("all")
+public class CipherTest1 {
+ public void test1(Cipher cipher, Key key) throws Exception {
+ cipher.init(Cipher.WRAP_MODE, key); // FLAG
+ }
+
+ public void test2(Cipher cipher, Key key, SecureRandom random) throws Exception {
+ cipher.init(Cipher.ENCRYPT_MODE, key, random);
+ }
+
+ public void setup(String transform) throws Exception {
+ Cipher cipher = Cipher.getInstance(transform);
+ }
+}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.class.data
new file mode 100644
index 0000000..fb46952
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.java.txt
new file mode 100644
index 0000000..5414ec8
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngCalls.java.txt
@@ -0,0 +1,26 @@
+package test.pkg;
+
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.KeyGenerator;
+
+public class PrngCalls {
+ public void testKeyGenerator() throws NoSuchAlgorithmException, NoSuchProviderException {
+ KeyGenerator generator = KeyGenerator.getInstance("AES", "BC");
+ generator.init(128);
+
+ KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(512);
+
+ KeyAgreement agreement = KeyAgreement.getInstance("DH", "BC");
+ agreement.generateSecret();
+
+ SecureRandom random = new SecureRandom();
+ byte bytes[] = new byte[20];
+ random.nextBytes(bytes);
+ }
+}
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandom.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandom.class.data
new file mode 100644
index 0000000..1741ccc
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandom.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandomProvider.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandomProvider.class.data
new file mode 100644
index 0000000..b1e6d3b
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround$LinuxPRNGSecureRandomProvider.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.class.data b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.class.data
new file mode 100644
index 0000000..32a785a
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.class.data
Binary files differ
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.java.txt
new file mode 100644
index 0000000..a63e267
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/bytecode/PrngWorkaround.java.txt
@@ -0,0 +1,336 @@
+/*
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will Google be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, as long as the origin is not misrepresented.
+ */
+
+package test.pkg;
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PrngWorkaround {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
+ getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PrngWorkaround() {}
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders =
+ Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(
+ secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng1.getProvider().getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG",
+ 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PrngWorkaround.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
\ No newline at end of file
diff --git a/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/CheckPermissions.java.txt b/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/CheckPermissions.java.txt
new file mode 100644
index 0000000..1d17110
--- /dev/null
+++ b/lint/cli/src/test/java/com/android/tools/lint/checks/data/src/test/pkg/CheckPermissions.java.txt
@@ -0,0 +1,17 @@
+package test.pkg;
+
+import android.view.View;
+
+public class CheckPermissions {
+ private void foo() {
+ context.checkCallingOrSelfPermission(Manifest.permission.INTERNET); // WRONG
+ context.checkPermission(Manifest.permission.INTERNET); // UNKNOWN (without type resolution)
+ check(context.checkCallingOrSelfPermission(Manifest.permission.INTERNET)); // OK
+ int check = context.checkCallingOrSelfPermission(Manifest.permission.INTERNET); // OK
+ if (context.checkCallingOrSelfPermission(Manifest.permission.INTERNET) // OK
+ != PackageManager.PERMISSION_GRANTED) {
+ Util.showAlert(context, "Error",
+ "Application requires permission to access the Internet");
+ }
+ }
+}
diff --git a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
index be52510..96a973d 100644
--- a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
+++ b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java
@@ -24,6 +24,7 @@
import com.android.tools.lint.detector.api.Detector.JavaScanner;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.JavaContext;
+import com.google.common.collect.Maps;
import java.io.File;
import java.util.ArrayList;
@@ -133,7 +134,7 @@
private static final int SAME_TYPE_COUNT = 8;
private final Map<String, List<VisitingDetector>> mMethodDetectors =
- new HashMap<String, List<VisitingDetector>>();
+ Maps.newHashMapWithExpectedSize(24);
private final List<VisitingDetector> mResourceFieldDetectors =
new ArrayList<VisitingDetector>();
private final List<VisitingDetector> mAllDetectors;
diff --git a/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java b/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java
index d91f423..623101f 100644
--- a/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java
+++ b/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java
@@ -23,6 +23,7 @@
import java.io.File;
+import lombok.ast.ClassDeclaration;
import lombok.ast.ConstructorDeclaration;
import lombok.ast.MethodDeclaration;
import lombok.ast.Node;
@@ -124,4 +125,21 @@
return null;
}
+
+ @Nullable
+ public static ClassDeclaration findSurroundingClass(Node scope) {
+ while (scope != null) {
+ Class<? extends Node> type = scope.getClass();
+ // The Lombok AST uses a flat hierarchy of node type implementation classes
+ // so no need to do instanceof stuff here.
+ if (type == ClassDeclaration.class) {
+ return (ClassDeclaration) scope;
+ }
+
+ scope = scope.getParent();
+ }
+
+ return null;
+ }
+
}
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java
index 4d70a8b..6bd30e2 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java
@@ -36,7 +36,7 @@
private static final List<Issue> sIssues;
static {
- final int initialCapacity = 158;
+ final int initialCapacity = 160;
List<Issue> issues = new ArrayList<Issue>(initialCapacity);
issues.add(AccessibilityDetector.ISSUE);
@@ -135,6 +135,8 @@
issues.add(SecurityDetector.WORLD_READABLE);
issues.add(SecurityDetector.WORLD_WRITEABLE);
issues.add(SecureRandomDetector.ISSUE);
+ issues.add(CheckPermissionDetector.ISSUE);
+ issues.add(SecureRandomGeneratorDetector.ISSUE);
issues.add(IconDetector.GIF_USAGE);
issues.add(IconDetector.ICON_DENSITIES);
issues.add(IconDetector.ICON_MISSING_FOLDER);
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/CheckPermissionDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/CheckPermissionDetector.java
new file mode 100644
index 0000000..f0a9233
--- /dev/null
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/CheckPermissionDetector.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.tools.lint.checks;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.tools.lint.detector.api.*;
+
+import lombok.ast.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Ensures that calls to check permission use the result (otherwise they probably meant to call the
+ * <b>enforce</b> permission methods instead)
+ */
+public class CheckPermissionDetector extends Detector implements Detector.JavaScanner {
+ /** Main issue checked by this detector */
+ public static final Issue ISSUE = Issue.create("UseCheckPermission", //$NON-NLS-1$
+ "Using the result of check permission calls",
+ "Ensures that the return value of check permission calls are used",
+
+ "You normally want to use the result of checking a permission; these methods " +
+ "return whether the permission is held; they do not throw an error if the permission " +
+ "is not granted. Code which does not do anything with the return value probably " +
+ "meant to be calling the enforce methods instead, e.g. rather than " +
+ "`Context#checkCallingPermission` it should call `Context#enforceCallingPermission`.",
+
+ Category.SECURITY, 6, Severity.WARNING,
+ new Implementation(CheckPermissionDetector.class, Scope.JAVA_FILE_SCOPE));
+
+ private static final String CHECK_PERMISSION = "checkPermission";
+
+ /**
+ * Constructs a new {@link com.android.tools.lint.checks.CheckPermissionDetector} check
+ */
+ public CheckPermissionDetector() {
+ }
+
+ // ---- Implements JavaScanner ----
+
+ @Override
+ public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor,
+ @NonNull MethodInvocation node) {
+ if (node.getParent() instanceof ExpressionStatement) {
+ String check = node.astName().astValue();
+ if (CHECK_PERMISSION.equals(check)) {
+ if (!ensureContextMethod(context, node)) {
+ return;
+ }
+ }
+ assert check.startsWith("check") : check;
+ String enforce = "enforce" + check.substring("check".length());
+ context.report(ISSUE, node, context.getLocation(node),
+ String.format(
+ "The result of %1$s is not used; did you mean to call %2$s?",
+ check, enforce), null);
+ }
+ }
+
+ private static boolean ensureContextMethod(
+ @NonNull JavaContext context,
+ @NonNull MethodInvocation node) {
+ // Method name used in many other contexts where it doesn't have the
+ // same semantics; only use this one if we can resolve types
+ // and we're certain this is the Context method
+ Node resolved = context.parser.resolve(context, node);
+ if (resolved instanceof MethodDeclaration) {
+ ClassDeclaration declaration = JavaContext.findSurroundingClass(resolved);
+ if (declaration != null && declaration.astName() != null) {
+ String className = declaration.astName().astValue();
+ if ("ContextWrapper".equals(className) || "Context".equals(className)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public List<String> getApplicableMethodNames() {
+ return Arrays.asList(
+ CHECK_PERMISSION,
+ "checkUriPermission",
+ "checkCallingOrSelfPermission",
+ "checkCallingPermission",
+ "checkCallingUriPermission",
+ "checkCallingOrSelfUriPermission"
+ );
+ }
+}
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java
index 33af4a0..b75ead7 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java
@@ -66,7 +66,7 @@
.addMoreInfo("http://developer.android.com/reference/java/security/SecureRandom.html");
private static final String SET_SEED = "setSeed"; //$NON-NLS-1$
- private static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom"; //$NON-NLS-1$
+ static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom"; //$NON-NLS-1$
private static final String OWNER_RANDOM = "java/util/Random"; //$NON-NLS-1$
private static final String VM_SECURE_RANDOM = 'L' + OWNER_SECURE_RANDOM + ';';
/** Method description for a method that takes a long argument (no return type specified */
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomGeneratorDetector.java b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomGeneratorDetector.java
new file mode 100644
index 0000000..7c9012d
--- /dev/null
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/SecureRandomGeneratorDetector.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.tools.lint.checks;
+
+import static com.android.SdkConstants.CONSTRUCTOR_NAME;
+import static com.android.tools.lint.checks.SecureRandomDetector.OWNER_SECURE_RANDOM;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.tools.lint.detector.api.Category;
+import com.android.tools.lint.detector.api.ClassContext;
+import com.android.tools.lint.detector.api.Context;
+import com.android.tools.lint.detector.api.Detector;
+import com.android.tools.lint.detector.api.Detector.ClassScanner;
+import com.android.tools.lint.detector.api.Implementation;
+import com.android.tools.lint.detector.api.Issue;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.tools.lint.detector.api.Location;
+import com.android.tools.lint.detector.api.Scope;
+import com.android.tools.lint.detector.api.Severity;
+
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.AbstractInsnNode;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.LdcInsnNode;
+import org.objectweb.asm.tree.MethodInsnNode;
+import org.objectweb.asm.tree.MethodNode;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Checks for pseudo random number generator initialization issues
+ */
+public class SecureRandomGeneratorDetector extends Detector implements ClassScanner {
+
+ @SuppressWarnings("SpellCheckingInspection")
+ private static final String BLOG_URL
+ = "https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html";
+
+ /** Whether the random number generator is initialized correctly */
+ public static final Issue ISSUE = Issue.create(
+ "TrulyRandom", //$NON-NLS-1$
+ "Weak RNG",
+ "Looks for calls to JCA primitives that may be affected by SecureRandom vulnerability",
+ "Key generation, signing, encryption, and random number generation may not " +
+ "receive cryptographically strong values due to improper initialization of " +
+ "the underlying PRNG on Android 4.3 and below.\n" +
+ "\n" +
+ "If your application relies on cryptographically secure random number generation " +
+ "you should apply the workaround described in " + BLOG_URL + " .\n" +
+ "\n" +
+ "This lint rule is mostly informational; it does not accurately detect whether " +
+ "cryptographically secure RNG is required, or whether the workaround has already " +
+ "been applied. After reading the blog entry and updating your code if necessary, " +
+ "you can disable this lint issue.",
+
+ Category.SECURITY,
+ 9,
+ Severity.WARNING,
+ new Implementation(
+ SecureRandomGeneratorDetector.class,
+ Scope.CLASS_FILE_SCOPE))
+ .addMoreInfo(BLOG_URL);
+
+ private static final String WRAP = "wrap"; //$NON-NLS-1$
+ private static final String UNWRAP = "unwrap"; //$NON-NLS-1$
+ private static final String INIT = "init"; //$NON-NLS-1$
+ private static final String INIT_SIGN = "initSign"; //$NON-NLS-1$
+ private static final String GET_INSTANCE = "getInstance"; //$NON-NLS-1$
+ private static final String FOR_NAME = "forName"; //$NON-NLS-1$
+ private static final String JAVA_LANG_CLASS = "java/lang/Class"; //$NON-NLS-1$
+ private static final String JAVAX_CRYPTO_KEY_GENERATOR = "javax/crypto/KeyGenerator";
+ private static final String JAVAX_CRYPTO_KEY_AGREEMENT = "javax/crypto/KeyAgreement";
+ private static final String JAVA_SECURITY_KEY_PAIR_GENERATOR =
+ "java/security/KeyPairGenerator";
+ private static final String JAVAX_CRYPTO_SIGNATURE = "javax/crypto/Signature";
+ private static final String JAVAX_CRYPTO_CIPHER = "javax/crypto/Cipher";
+ private static final String JAVAX_NET_SSL_SSLENGINE = "javax/net/ssl/SSLEngine";
+
+ /** Constructs a new {@link SecureRandomGeneratorDetector} */
+ public SecureRandomGeneratorDetector() {
+ }
+
+ // ---- Implements ClassScanner ----
+
+ @Nullable
+ @Override
+ public List<String> getApplicableCallOwners() {
+ return Arrays.asList(
+ JAVAX_CRYPTO_KEY_GENERATOR,
+ JAVA_SECURITY_KEY_PAIR_GENERATOR,
+ JAVAX_CRYPTO_KEY_AGREEMENT,
+ OWNER_SECURE_RANDOM,
+ JAVAX_NET_SSL_SSLENGINE,
+ JAVAX_CRYPTO_SIGNATURE,
+ JAVAX_CRYPTO_CIPHER
+ );
+ }
+
+ @Nullable
+ @Override
+ public List<String> getApplicableCallNames() {
+ return Collections.singletonList(FOR_NAME);
+ }
+
+ /** Location of first call to key generator (etc), if any */
+ private Location mLocation;
+
+ /** Whether the issue should be ignored (because we have a workaround, or because
+ * we're only targeting correct implementations, etc */
+ private boolean mIgnore;
+
+ @Override
+ public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode,
+ @NonNull MethodNode method, @NonNull MethodInsnNode call) {
+ if (mIgnore) {
+ return;
+ }
+
+ String owner = call.owner;
+ String name = call.name;
+
+ // Look for the workaround code: if we see a Class.forName on the harmony NativeCrypto,
+ // we'll consider that a sign.
+
+ if (name.equals(FOR_NAME)) {
+ if (call.getOpcode() != Opcodes.INVOKESTATIC ||
+ !owner.equals(JAVA_LANG_CLASS)) {
+ return;
+ }
+ AbstractInsnNode prev = LintUtils.getPrevInstruction(call);
+ if (prev instanceof LdcInsnNode) {
+ Object cst = ((LdcInsnNode)prev).cst;
+ //noinspection SpellCheckingInspection
+ if (cst instanceof String &&
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto".equals(cst)) {
+ mIgnore = true;
+ }
+ }
+ return;
+ }
+
+ // Look for calls that probably require a properly initialized random number generator.
+ assert owner.equals(JAVAX_CRYPTO_KEY_GENERATOR)
+ || owner.equals(JAVA_SECURITY_KEY_PAIR_GENERATOR)
+ || owner.equals(JAVAX_CRYPTO_KEY_AGREEMENT)
+ || owner.equals(OWNER_SECURE_RANDOM)
+ || owner.equals(JAVAX_CRYPTO_CIPHER)
+ || owner.equals(JAVAX_CRYPTO_SIGNATURE)
+ || owner.equals(JAVAX_NET_SSL_SSLENGINE) : owner;
+ boolean warn = false;
+
+ if (owner.equals(JAVAX_CRYPTO_SIGNATURE)) {
+ warn = name.equals(INIT_SIGN);
+ } else if (owner.equals(JAVAX_CRYPTO_CIPHER)) {
+ if (name.equals(INIT)) {
+ int arity = getDescArity(call.desc);
+ AbstractInsnNode node = call;
+ for (int i = 0; i < arity; i++) {
+ node = LintUtils.getPrevInstruction(node);
+ if (node == null) {
+ break;
+ }
+ }
+ if (node != null) {
+ int opcode = node.getOpcode();
+ if (opcode == Opcodes.ICONST_3 || // Cipher.WRAP_MODE
+ opcode == Opcodes.ICONST_1) { // Cipher.ENCRYPT_MODE
+ warn = true;
+ }
+ }
+ }
+ } else if (name.equals(GET_INSTANCE) || name.equals(CONSTRUCTOR_NAME)
+ || name.equals(WRAP) || name.equals(UNWRAP)) { // For SSLEngine
+ warn = true;
+ }
+
+ if (warn) {
+ if (mLocation != null) {
+ return;
+ }
+ if (context.getMainProject().getMinSdk() > 18) {
+ // Fix no longer needed
+ mIgnore = true;
+ return;
+ }
+
+ if (context.getDriver().isSuppressed(ISSUE, classNode, method, call)) {
+ mIgnore = true;
+ } else {
+ mLocation = context.getLocation(call);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static int getDescArity(String desc) {
+ int arity = 0;
+ // For example, (ILjava/security/Key;)V => 2
+ for (int i = 1, max = desc.length(); i < max; i++) {
+ char c = desc.charAt(i);
+ if (c == ')') {
+ break;
+ } else if (c == 'L') {
+ arity++;
+ i = desc.indexOf(';', i);
+ assert i != -1 : desc;
+ } else {
+ arity++;
+ }
+ }
+
+ return arity;
+ }
+
+ @Override
+ public void afterCheckProject(@NonNull Context context) {
+ if (mLocation != null && !mIgnore) {
+ String message = "Potentially insecure random numbers on Android 4.3 and older. "
+ + "Read " + BLOG_URL + " for more info.";
+ context.report(ISSUE, mLocation, message, null);
+ }
+ }
+}
diff --git a/sdk-common/sdk-common.iml b/sdk-common/sdk-common.iml
index 03a2b95..4f564af 100644
--- a/sdk-common/sdk-common.iml
+++ b/sdk-common/sdk-common.iml
@@ -5,7 +5,7 @@
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
- <sourceFolder url="file://$MODULE_DIR$/src/test/resources" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
<excludeFolder url="file://$MODULE_DIR$/.settings" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
diff --git a/sdk-common/src/main/java/com/android/ide/common/packaging/PackagingUtils.java b/sdk-common/src/main/java/com/android/ide/common/packaging/PackagingUtils.java
index 0471555..b41b84e 100644
--- a/sdk-common/src/main/java/com/android/ide/common/packaging/PackagingUtils.java
+++ b/sdk-common/src/main/java/com/android/ide/common/packaging/PackagingUtils.java
@@ -72,8 +72,8 @@
!"swp".equalsIgnoreCase(extension) && // vi swap file
!"thumbs.db".equalsIgnoreCase(fileName) && // image index file
!"picasa.ini".equalsIgnoreCase(fileName) && // image index file
+ !"about.html".equalsIgnoreCase(fileName) && // Javadoc
!"package.html".equalsIgnoreCase(fileName) && // Javadoc
!"overview.html".equalsIgnoreCase(fileName); // Javadoc
-
}
}
diff --git a/sdk-common/src/main/java/com/android/ide/common/rendering/HardwareConfigHelper.java b/sdk-common/src/main/java/com/android/ide/common/rendering/HardwareConfigHelper.java
index 1e5a760..aff9780 100644
--- a/sdk-common/src/main/java/com/android/ide/common/rendering/HardwareConfigHelper.java
+++ b/sdk-common/src/main/java/com/android/ide/common/rendering/HardwareConfigHelper.java
@@ -271,10 +271,10 @@
if (id.equals("Nexus S")) { //$NON-NLS-1$
return 2;
}
- if (id.equals("Galaxy Nexus")) { //$NON-NLS-1$
+ if (id.equals("Galaxy Nexus")) { //$NON-NLS-1$
return 3;
}
- if (id.equals("Nexus 7")) { //$NON-NLS-1$
+ if (id.equals("Nexus 7")) { //$NON-NLS-1$
return 4; // 2012 version
}
if (id.equals("Nexus 10")) { //$NON-NLS-1$
@@ -283,11 +283,14 @@
if (id.equals("Nexus 4")) { //$NON-NLS-1$
return 6;
}
- if (id.equals("Nexus 7 2013")) { //$NON-NLS-1$
+ if (id.equals("Nexus 7 2013")) { //$NON-NLS-1$
return 7;
}
+ if (id.equals("Nexus 5")) { //$NON-NLS-1$
+ return 8;
+ }
- return 8; // devices released in the future?
+ return 100; // devices released in the future?
}
/**
diff --git a/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityException.java b/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityException.java
new file mode 100644
index 0000000..38dcce9
--- /dev/null
+++ b/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityException.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ide.common.rendering;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+/** Exception thrown when custom view code makes an illegal code while rendering under layoutlib */
+public class RenderSecurityException extends SecurityException {
+
+ private final String myMessage;
+
+ /** Use one of the create factory methods */
+ private RenderSecurityException(@NonNull String message) {
+ super(message);
+ myMessage = message;
+ }
+
+ @Override
+ public String getMessage() {
+ return myMessage;
+ }
+
+ @Override
+ public String toString() {
+ // super prepends the fully qualified name of the exception
+ return getMessage();
+ }
+ /**
+ * Creates a new {@linkplain RenderSecurityException}
+ *
+ * @param resource the type of resource being accessed - "Thread", "Write", "Socket", etc
+ * @param context more information about the object, such as the path of the file being read
+ * @return a new exception
+ */
+ @NonNull
+ public static RenderSecurityException create(@NonNull String resource,
+ @Nullable String context) {
+ return new RenderSecurityException(computeLabel(resource, context));
+ }
+
+ /**
+ * Creates a new {@linkplain RenderSecurityException}
+ *
+ * @param message the message for the exception
+ * @return a new exception
+ */
+ @NonNull
+ public static RenderSecurityException create(@NonNull String message) {
+ return new RenderSecurityException(message);
+ }
+
+ private static String computeLabel(@NonNull String resource, @Nullable String context) {
+ StringBuilder sb = new StringBuilder(40);
+ sb.append(resource);
+ sb.append(" access not allowed during rendering");
+ if (context != null) {
+ sb.append(" (").append(context).append(")");
+ }
+ return sb.toString();
+ }
+}
diff --git a/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityManager.java b/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityManager.java
new file mode 100644
index 0000000..1126777
--- /dev/null
+++ b/sdk-common/src/main/java/com/android/ide/common/rendering/RenderSecurityManager.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ide.common.rendering;
+
+import static com.android.SdkConstants.DOT_CLASS;
+import static com.android.SdkConstants.DOT_JAR;
+import static com.android.SdkConstants.VALUE_FALSE;
+
+import com.android.annotations.Nullable;
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FilePermission;
+import java.net.InetAddress;
+import java.security.Permission;
+
+/**
+ * A {@link java.lang.SecurityManager} which is used for layout lib rendering, to
+ * prevent custom views from accidentally exiting the whole IDE if they call
+ * {@code System.exit}, as well as unintentionally writing files etc.
+ * <p>
+ * The security manager only checks calls on the current thread for which it
+ * was made active with a call to {@link #setActive(boolean)}, as well as any
+ * threads constructed from the render thread.
+ */
+public class RenderSecurityManager extends SecurityManager {
+ /** Property used to disable sandbox */
+ public static final String ENABLED_PROPERTY = "android.render.sandbox";
+
+ /** Whether we should restrict reading to certain paths */
+ public static final boolean RESTRICT_READS = false;
+
+ /**
+ * Whether the security manager is enabled for this session (it might still
+ * be inactive, either because it's active for a different thread, or because
+ * it has been disabled via {@link #setActive(boolean)} (which sets the
+ * per-instance mEnabled flag)
+ */
+ public static boolean sEnabled =
+ !VALUE_FALSE.equals(System.getProperty(ENABLED_PROPERTY));
+
+ /**
+ * Thread local data which indicates whether the current thread is relevant for
+ * this security manager. This is an inheritable thread local such that any threads
+ * spawned from this thread will also apply the security manager; otherwise code
+ * could just create new threads and execute code separate from the security manager
+ * there.
+ */
+ private static ThreadLocal<Boolean> sIsRenderThread = new InheritableThreadLocal<Boolean>() {
+ @Override protected synchronized Boolean initialValue() {
+ return Boolean.FALSE;
+ }
+ @Override protected synchronized Boolean childValue(Boolean parentValue) {
+ return parentValue;
+ }
+ };
+
+ private boolean mAllowSetSecurityManager;
+ private boolean mDisabled;
+ private String mSdkPath;
+ private String mProjectPath;
+ private String mTempDir;
+ private SecurityManager myPreviousSecurityManager;
+ private ILogger mLogger;
+
+ /**
+ * Returns the current render security manager, if any. This will only return
+ * non-null if there is an active {@linkplain RenderSecurityManager} as the
+ * current global security manager.
+ */
+ @Nullable
+ public static RenderSecurityManager getCurrent() {
+ if (sIsRenderThread.get()) {
+ SecurityManager securityManager = System.getSecurityManager();
+ if (securityManager instanceof RenderSecurityManager) {
+ return (RenderSecurityManager) securityManager;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates a security manager suitable for controlling access to custom views
+ * being rendered by layoutlib, ensuring that they don't accidentally try to
+ * write files etc (which could corrupt data if they for example assume device
+ * paths that are not the same for the running IDE; for example, they could try
+ * to clear out their own local app storage, which in the IDE could be the
+ * user's home directory.)
+ * <p>
+ * Note: By default a security manager is not active. You need to call
+ * {@link #setActive(boolean)} with true to activate it, <b>instead</b> of just calling
+ * {@link System#setSecurityManager(SecurityManager)}.
+ *
+ * @param sdkPath an optional path to the SDK install being used by layoutlib;
+ * this is used to white-list path prefixes for layoutlib resource
+ * lookup
+ * @param projectPath a path to the project directory, used for similar purposes
+ */
+ public RenderSecurityManager(
+ @Nullable String sdkPath,
+ @Nullable String projectPath) {
+ mSdkPath = sdkPath;
+ mProjectPath = projectPath;
+ mTempDir = System.getProperty("java.io.tmpdir");
+ }
+
+ /** Sets an optional logger. Returns this for constructor chaining. */
+ public RenderSecurityManager setLogger(@Nullable ILogger logger) {
+ mLogger = logger;
+ return this;
+ }
+
+ public void setActive(boolean active) {
+ SecurityManager current = System.getSecurityManager();
+ boolean isActive = current == this;
+ if (active == isActive) {
+ return;
+ }
+
+ if (active) {
+ // Enable
+ assert !(current instanceof RenderSecurityManager);
+ myPreviousSecurityManager = current;
+ sIsRenderThread.set(true);
+ mDisabled = false;
+ System.setSecurityManager(this);
+ } else {
+ // Disable
+ mAllowSetSecurityManager = true;
+ // Don't set mDisabled and clear sInRenderThread yet: the call
+ // to revert to the previous security manager below will trigger
+ // a check permission, and in that code we need to distinguish between
+ // this call (isRelevant() should return true) and other threads calling
+ // it outside the scope of the security manager
+ try {
+ // Only reset the security manager if it hasn't already been set to
+ // something else. If other threads try to do the same thing we could have
+ // a problem; if they sampled the render security manager while it was globally
+ // active, replaced it with their own, and sometime in the future try to
+ // set it back, it will be active when we didn't intend for it to be. That's
+ // why there is also the {@code mDisabled} flag, used to ignore any requests
+ // later on.
+ if (current instanceof RenderSecurityManager) {
+ System.setSecurityManager(myPreviousSecurityManager);
+ } else if (mLogger != null) {
+ sIsRenderThread.set(false);
+ mLogger.warning("Security manager was changed behind the scenes: ", current);
+ }
+ } finally {
+ mDisabled = true;
+ mAllowSetSecurityManager = false;
+ sIsRenderThread.set(false);
+ }
+ }
+ }
+
+ private boolean isRelevant() {
+ return sEnabled && !mDisabled && sIsRenderThread.get();
+ }
+
+ public void dispose() {
+ setActive(false);
+ }
+
+ // Permitted by custom views: access any package or member, read properties
+
+ @Override
+ public void checkPackageAccess(String pkg) {
+ }
+
+ @Override
+ public void checkMemberAccess(Class<?> clazz, int which) {
+ }
+
+ @Override
+ public void checkPropertyAccess(String property) {
+ }
+
+ @Override
+ public void checkLink(String lib) {
+ // Needed to for example load the "fontmanager" library from layout lib (from the
+ // BiDiRenderer's layoutGlyphVector call
+ }
+
+ @Override
+ public void checkCreateClassLoader() {
+ // TODO: Layoutlib makes heavy use of this, so we can't block it yet.
+ // To fix this we should make a local class loader, passed to layoutlib, which
+ // knows how to reset the security manager
+ }
+
+ //------------------------------------------------------------------------------------------
+ // Reading is permitted for certain files only
+ //------------------------------------------------------------------------------------------
+
+ @SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"})
+ @Override
+ public void checkRead(String file) {
+ if (RESTRICT_READS && isRelevant() && !isReadingAllowed(file)) {
+ throw RenderSecurityException.create("Read", file);
+ }
+ }
+
+ @SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"})
+ @Override
+ public void checkRead(String file, Object context) {
+ if (RESTRICT_READS && isRelevant() && !isReadingAllowed(file)) {
+ throw RenderSecurityException.create("Read", file);
+ }
+ }
+
+ private boolean isReadingAllowed(String path) {
+ if (RESTRICT_READS) {
+ // Allow reading files in the SDK install (fonts etc)
+ if (mSdkPath != null && path.startsWith(mSdkPath)) {
+ return true;
+ }
+
+ // Allowing reading resources in the project, such as icons
+ if (mProjectPath != null && path.startsWith(mProjectPath)) {
+ return true;
+ }
+
+ if (path.startsWith("#") && path.indexOf(File.separatorChar) == -1) {
+ // It's really layoutlib's ResourceHelper.getColorStateList which calls isFile()
+ // on values to see if it's a file or a color.
+ return true;
+ }
+
+ // Needed by layoutlib's class loader. Note that we've locked down the ability to
+ // create new class loaders.
+ if (path.endsWith(DOT_CLASS) || path.endsWith(DOT_JAR)) {
+ return true;
+ }
+
+ // Allow reading files in temp
+ if (path.startsWith(mTempDir)) {
+ return true;
+ }
+
+ String javaHome = System.getProperty("java.home");
+ if (path.startsWith(javaHome)) { // Allow JDK to load its own classes
+ return true;
+ } else if (javaHome.endsWith("/Contents/Home")) {
+ // On Mac, Home lives two directory levels down from the real home, and we
+ // sometimes need to read from sibling directories (e.g. ../Libraries/ etc)
+ if (path.regionMatches(0, javaHome, 0, javaHome.length() -
+ "Contents/Home".length())) {
+ return true;
+ }
+ }
+
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private boolean isWritingAllowed(String path) {
+ //noinspection RedundantIfStatement
+ if (path.startsWith(mTempDir)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ //------------------------------------------------------------------------------------------
+ // Not permitted:
+ //------------------------------------------------------------------------------------------
+
+ @Override
+ public void checkExit(int status) {
+ // Probably not intentional in a custom view; would take down the whole IDE!
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Exit", String.valueOf(status));
+ }
+
+ super.checkExit(status);
+ }
+
+ @Override
+ public void checkPropertiesAccess() {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Property", null);
+ }
+ }
+
+ // Prevent code execution/linking/loading
+
+ @Override
+ public void checkPackageDefinition(String pkg) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Package", pkg);
+ }
+ }
+
+ @Override
+ public void checkExec(String cmd) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Exec", cmd);
+ }
+ }
+
+ // Prevent network access
+
+ @Override
+ public void checkConnect(String host, int port) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", host + ":" + port);
+ }
+ }
+
+ @Override
+ public void checkConnect(String host, int port, Object context) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", host + ":" + port);
+ }
+ }
+
+ @Override
+ public void checkListen(int port) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", "port " + port);
+ }
+ }
+
+ @Override
+ public void checkAccept(String host, int port) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", host + ":" + port);
+ }
+ }
+
+ @Override
+ public void checkSetFactory() {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", null);
+ }
+ }
+
+ @Override
+ public void checkMulticast(InetAddress inetAddress) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", inetAddress.getCanonicalHostName());
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void checkMulticast(InetAddress inetAddress, byte ttl) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Socket", inetAddress.getCanonicalHostName());
+ }
+ }
+
+ // Prevent file access
+
+ @Override
+ public void checkDelete(String file) {
+ if (isRelevant()) {
+ // Allow writing to temp
+ if (isWritingAllowed(file)) {
+ return;
+ }
+
+ throw RenderSecurityException.create("Delete", file);
+ }
+ }
+
+ @Override
+ public void checkAwtEventQueueAccess() {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Event", null);
+ }
+ }
+
+ // Prevent writes
+
+ @Override
+ public void checkWrite(FileDescriptor fileDescriptor) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Write", fileDescriptor.toString());
+ }
+ }
+
+ @Override
+ public void checkWrite(String file) {
+ if (isRelevant()) {
+ if (isWritingAllowed(file)) {
+ return;
+ }
+
+ throw RenderSecurityException.create("Write", file);
+ }
+ }
+
+ // Misc
+
+ @Override
+ public void checkPrintJobAccess() {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Print", null);
+ }
+ }
+
+ @Override
+ public void checkSystemClipboardAccess() {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Clipboard", null);
+ }
+ }
+
+ @Override
+ public boolean checkTopLevelWindow(Object context) {
+ if (isRelevant()) {
+ throw RenderSecurityException.create("Window", null);
+ }
+ return false;
+ }
+
+ @Override
+ public void checkAccess(Thread thread) {
+ // Turns out layoutlib sometimes creates asynchronous calls, for example
+ // java.lang.Thread.<init>(Thread.java:521)
+ // at android.os.AsyncTask$1.newThread(AsyncTask.java:189)
+ // at java.util.concurrent.ThreadPoolExecutor.addThread(ThreadPoolExecutor.java:670)
+ // at java.util.concurrent.ThreadPoolExecutor.addIfUnderCorePoolSize(ThreadPoolExecutor.java:706)
+ // at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:650)
+ // at android.os.AsyncTask$SerialExecutor.scheduleNext(AsyncTask.java:244)
+ // at android.os.AsyncTask$SerialExecutor.execute(AsyncTask.java:238)
+ // at android.os.AsyncTask.execute(AsyncTask.java:604)
+ // at android.widget.TextView.updateTextServicesLocaleAsync(TextView.java:8078)
+
+ // This may not work correctly for render sessions, which are treated as synchronous
+ // by callers. We should re-enable these checks to chase down these calls and
+ // eliminate them from layoutlib, but until we do, it's necessary to allow thread
+ // creation.
+ }
+
+ @Override
+ public void checkAccess(ThreadGroup threadGroup) {
+ // See checkAccess(Thread)
+ }
+
+ @Override
+ public void checkPermission(Permission permission) {
+ String name = permission.getName();
+ if ("setSecurityManager".equals(name)) {
+ if (isRelevant()) {
+ if (!mAllowSetSecurityManager) {
+ throw RenderSecurityException.create("Security", null);
+ }
+ } else if (mLogger != null) {
+ mLogger.warning("RenderSecurityManager being replaced by another thread");
+ }
+ } else if (isRelevant()) {
+ String actions = permission.getActions();
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (RESTRICT_READS && "read".equals(actions)) {
+ if (!isReadingAllowed(name)) {
+ throw RenderSecurityException.create("Read", name);
+ }
+ } else if (!actions.isEmpty() && !actions.equals("read")) {
+ // write, execute, delete, readlink
+ if (!(permission instanceof FilePermission) || !isWritingAllowed(name)) {
+ throw RenderSecurityException.create("Write", name);
+ }
+ }
+ }
+ }
+}
diff --git a/sdk-common/src/main/java/com/android/ide/common/res2/NodeUtils.java b/sdk-common/src/main/java/com/android/ide/common/res2/NodeUtils.java
index e977bdf..f945cdb 100644
--- a/sdk-common/src/main/java/com/android/ide/common/res2/NodeUtils.java
+++ b/sdk-common/src/main/java/com/android/ide/common/res2/NodeUtils.java
@@ -80,7 +80,15 @@
NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
for (int i = 0, n = attributes.getLength(); i < n; i++) {
- processSingleNodeNamespace(attributes.item(i), document);
+ Node attribute = attributes.item(i);
+ if (!processSingleNodeNamespace(attribute, document)) {
+ String nsUri = attribute.getNamespaceURI();
+ if (nsUri != null) {
+ attributes.removeNamedItemNS(nsUri, attribute.getLocalName());
+ } else {
+ attributes.removeNamedItem(attribute.getLocalName());
+ }
+ }
}
}
@@ -98,10 +106,17 @@
/**
* Update the namespace of a given node to work with a given document.
+ *
* @param node the node to update
* @param document the new document
+ *
+ * @return false if the attribute is to be dropped
*/
- private static void processSingleNodeNamespace(Node node, Document document) {
+ private static boolean processSingleNodeNamespace(Node node, Document document) {
+ if ("xmlns".equals(node.getLocalName())) {
+ return false;
+ }
+
String ns = node.getNamespaceURI();
if (ns != null) {
NamedNodeMap docAttributes = document.getAttributes();
@@ -118,6 +133,8 @@
prefix = prefix.substring(6);
node.setPrefix(prefix);
}
+
+ return true;
}
/**
diff --git a/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java b/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
index a8e39ee..c470aa4 100644
--- a/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
+++ b/sdk-common/src/main/java/com/android/ide/common/resources/ResourceResolver.java
@@ -16,6 +16,7 @@
package com.android.ide.common.resources;
+import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX;
import static com.android.SdkConstants.PREFIX_ANDROID;
import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
import static com.android.SdkConstants.REFERENCE_STYLE;
@@ -30,8 +31,10 @@
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
public class ResourceResolver extends RenderResources {
public static final String THEME_NAME = "Theme";
@@ -182,6 +185,11 @@
@Override
public ResourceValue findItemInStyle(StyleResourceValue style, String itemName,
boolean isFrameworkAttr) {
+ return findItemInStyle(style, itemName, isFrameworkAttr, 0);
+ }
+
+ private ResourceValue findItemInStyle(StyleResourceValue style, String itemName,
+ boolean isFrameworkAttr, int depth) {
ResourceValue item = style.findValue(itemName, isFrameworkAttr);
// if we didn't find it, we look in the parent style (if applicable)
@@ -189,13 +197,63 @@
if (item == null) {
StyleResourceValue parentStyle = mStyleInheritanceMap.get(style);
if (parentStyle != null) {
- return findItemInStyle(parentStyle, itemName, isFrameworkAttr);
+ if (depth >= MAX_RESOURCE_INDIRECTION) {
+ if (mLogger != null) {
+ mLogger.error(LayoutLog.TAG_BROKEN,
+ String.format("Cyclic style parent definitions: %1$s",
+ computeCyclicStyleChain(style)),
+ null);
+ }
+
+ return null;
+ }
+
+ return findItemInStyle(parentStyle, itemName, isFrameworkAttr, depth + 1);
}
}
return item;
}
+ private String computeCyclicStyleChain(StyleResourceValue style) {
+ StringBuilder sb = new StringBuilder(100);
+ appendStyleParents(style, new HashSet<StyleResourceValue>(), 0, sb);
+ return sb.toString();
+ }
+
+ private void appendStyleParents(StyleResourceValue style, Set<StyleResourceValue> seen,
+ int depth, StringBuilder sb) {
+ if (depth >= MAX_RESOURCE_INDIRECTION) {
+ sb.append("...");
+ return;
+ }
+
+ boolean haveSeen = seen.contains(style);
+ seen.add(style);
+
+ sb.append('"');
+ if (style.isFramework()) {
+ sb.append(PREFIX_ANDROID);
+ }
+ sb.append(style.getName());
+ sb.append('"');
+
+ if (haveSeen) {
+ return;
+ }
+
+ StyleResourceValue parentStyle = mStyleInheritanceMap.get(style);
+ if (parentStyle != null) {
+ if (style.getParentStyle() != null) {
+ sb.append(" specifies parent ");
+ } else {
+ sb.append(" implies parent ");
+ }
+
+ appendStyleParents(parentStyle, seen, depth + 1, sb);
+ }
+ }
+
@Override
public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) {
if (reference == null) {
@@ -357,7 +415,6 @@
// didn't find the resource anywhere.
return null;
-
}
/**
@@ -547,6 +604,27 @@
}
/**
+ * Returns true if the given {@code themeStyle} extends the theme given by
+ * {@code parentStyle}
+ */
+ public boolean themeExtends(@NonNull String parentStyle, @NonNull String themeStyle) {
+ ResourceValue parentValue = findResValue(parentStyle, parentStyle.startsWith(ANDROID_STYLE_RESOURCE_PREFIX));
+ if (parentValue instanceof StyleResourceValue) {
+ ResourceValue themeValue = findResValue(themeStyle,
+ themeStyle.startsWith(ANDROID_STYLE_RESOURCE_PREFIX));
+ if (themeValue == parentValue) {
+ return true;
+ }
+ if (themeValue instanceof StyleResourceValue) {
+ return themeIsParentOf((StyleResourceValue) parentValue,
+ (StyleResourceValue) themeValue);
+ }
+ }
+
+ return false;
+ }
+
+ /**
* Creates a new {@link ResourceResolver} which records all resource resolution
* lookups into the given list. Note that it is the responsibility of the caller
* to clear/reset the list between subsequent lookup operations.
diff --git a/sdk-common/src/main/java/com/android/ide/common/resources/configuration/FolderConfiguration.java b/sdk-common/src/main/java/com/android/ide/common/resources/configuration/FolderConfiguration.java
index 96031fc..fced184 100644
--- a/sdk-common/src/main/java/com/android/ide/common/resources/configuration/FolderConfiguration.java
+++ b/sdk-common/src/main/java/com/android/ide/common/resources/configuration/FolderConfiguration.java
@@ -656,7 +656,26 @@
return result.toString();
}
- /**
+ /**
+ * Returns the folder configuration as a unique key
+ */
+ public String getUniqueKey() {
+ StringBuilder result = new StringBuilder(100);
+
+ for (ResourceQualifier qualifier : mQualifiers) {
+ if (qualifier != null) {
+ String segment = qualifier.getFolderSegment();
+ if (segment != null && !segment.isEmpty()) {
+ result.append(SdkConstants.RES_QUALIFIER_SEP);
+ result.append(segment);
+ }
+ }
+ }
+
+ return result.toString();
+ }
+
+ /**
* Returns {@link #toDisplayString()}.
*/
@Override
@@ -730,7 +749,7 @@
return "default";
}
- StringBuilder result = new StringBuilder();
+ StringBuilder result = new StringBuilder(100);
int index = 0;
// pre- language/region qualifiers
diff --git a/sdk-common/src/main/java/com/android/ide/common/sdk/SdkVersionInfo.java b/sdk-common/src/main/java/com/android/ide/common/sdk/SdkVersionInfo.java
index a55db2a..70b5749 100644
--- a/sdk-common/src/main/java/com/android/ide/common/sdk/SdkVersionInfo.java
+++ b/sdk-common/src/main/java/com/android/ide/common/sdk/SdkVersionInfo.java
@@ -28,7 +28,7 @@
* release. This number is used as a baseline and any more recent platforms
* found can be used to increase the highest known number.
*/
- public static final int HIGHEST_KNOWN_API = 18;
+ public static final int HIGHEST_KNOWN_API = 19;
/**
* Returns the Android version and code name of the given API level, or null
@@ -60,6 +60,7 @@
case 16: return "API 16: Android 4.1 (Jelly Bean)";
case 17: return "API 17: Android 4.2 (Jelly Bean)";
case 18: return "API 18: Android 4.3 (Jelly Bean)";
+ case 19: return "API 19: Android 4.4 (KitKat)";
// If you add more versions here, also update #getBuildCodes and
// #HIGHEST_KNOWN_API
@@ -98,6 +99,7 @@
case 16: return "JELLY_BEAN"; //$NON-NLS-1$
case 17: return "JELLY_BEAN_MR1"; //$NON-NLS-1$
case 18: return "JELLY_BEAN_MR2"; //$NON-NLS-1$
+ case 19: return "KITKAT"; //$NON-NLS-1$
// If you add more versions here, also update #getAndroidName and
// #HIGHEST_KNOWN_API
}
@@ -118,7 +120,7 @@
*/
public static int getApiByBuildCode(String buildCode, boolean recognizeUnknowns) {
for (int api = 1; api <= HIGHEST_KNOWN_API; api++) {
- String code = SdkVersionInfo.getBuildCode(api);
+ String code = getBuildCode(api);
if (code != null && code.equalsIgnoreCase(buildCode)) {
return api;
}
diff --git a/sdk-common/src/test/java/com/android/ide/common/rendering/HardwareConfigHelperTest.java b/sdk-common/src/test/java/com/android/ide/common/rendering/HardwareConfigHelperTest.java
index 2c82a89..2c4b354 100644
--- a/sdk-common/src/test/java/com/android/ide/common/rendering/HardwareConfigHelperTest.java
+++ b/sdk-common/src/test/java/com/android/ide/common/rendering/HardwareConfigHelperTest.java
@@ -71,7 +71,7 @@
public void testNexusRank() {
List<Device> devices = Lists.newArrayList();
DeviceManager deviceManager = getDeviceManager();
- for (String id : new String[] { "Nexus 7 2013", "Nexus 10", "Nexus 4", "Nexus 7",
+ for (String id : new String[] { "Nexus 7 2013", "Nexus 5", "Nexus 10", "Nexus 4", "Nexus 7",
"Galaxy Nexus", "Nexus S", "Nexus One"}) {
Device device = deviceManager.getDevice(id, "Google");
assertNotNull(device);
@@ -94,8 +94,8 @@
"Nexus 7",
"Nexus 10",
"Nexus 4",
- "Nexus 7 2013"
-
+ "Nexus 7 2013",
+ "Nexus 5"
), ids);
}
}
diff --git a/sdk-common/src/test/java/com/android/ide/common/rendering/RenderSecurityManagerTest.java b/sdk-common/src/test/java/com/android/ide/common/rendering/RenderSecurityManagerTest.java
new file mode 100644
index 0000000..c1ea6ef
--- /dev/null
+++ b/sdk-common/src/test/java/com/android/ide/common/rendering/RenderSecurityManagerTest.java
@@ -0,0 +1,540 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.ide.common.rendering;
+
+import com.android.ide.common.res2.RecordingLogger;
+import com.google.common.io.Files;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.FilePermission;
+import java.security.Permission;
+import java.util.Collections;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class RenderSecurityManagerTest extends TestCase {
+
+ public void testExec() throws Exception {
+ assertNull(RenderSecurityManager.getCurrent());
+
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ RecordingLogger logger = new RecordingLogger();
+ manager.setLogger(logger);
+ try {
+ assertNull(RenderSecurityManager.getCurrent());
+ manager.setActive(true);
+ assertSame(manager, RenderSecurityManager.getCurrent());
+ if (new File("/bin/ls").exists()) {
+ Runtime.getRuntime().exec("/bin/ls");
+ } else {
+ manager.checkExec("/bin/ls");
+ }
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ //noinspection ConstantConditions
+ assertEquals(
+ RenderSecurityManager.RESTRICT_READS ?
+ "Read access not allowed during rendering (/bin/ls)" :
+ "Exec access not allowed during rendering (/bin/ls)",
+ exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+ assertEquals(Collections.<String>emptyList(), logger.getWarningMsgs());
+ }
+ }
+
+ public void testSetSecurityManager() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+ System.setSecurityManager(null);
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Security access not allowed during rendering", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testReadWrite() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+ manager.checkPermission(new FilePermission("/foo", "read,write"));
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testExecute() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+ manager.checkPermission(new FilePermission("/foo", "execute"));
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testDelete() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+ manager.checkPermission(new FilePermission("/foo", "delete"));
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Write access not allowed during rendering (/foo)", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testInvalidRead() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ if (RenderSecurityManager.RESTRICT_READS) {
+ try {
+ File file = new File(System.getProperty("user.home"));
+ //noinspection ResultOfMethodCallIgnored
+ file.lastModified();
+
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Read access not allowed during rendering (" +
+ System.getProperty("user.home") + ")", exception.toString());
+ // pass
+ }
+ } else {
+ try {
+ File file = new File(System.getProperty("user.home"));
+ //noinspection ResultOfMethodCallIgnored
+ file.lastModified();
+ } catch (SecurityException exception) {
+ fail("Reading should be allowed");
+ }
+ }
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testInvalidPropertyWrite() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ // Try to make java.io.tmpdir point to user.home to grant myself access:
+ String userHome = System.getProperty("user.home");
+ System.setProperty("java.io.tmpdir", userHome);
+
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Write access not allowed during rendering (java.io.tmpdir)",
+ exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testReadOk() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ File jdkHome = new File(System.getProperty("java.home"));
+ assertTrue(jdkHome.exists());
+ //noinspection ResultOfMethodCallIgnored
+ File[] files = jdkHome.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile()) {
+ Files.toByteArray(file);
+ }
+ }
+ }
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testProperties() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ System.getProperties();
+
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Property access not allowed during rendering", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testExit() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ System.exit(-1);
+
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ assertEquals("Exit access not allowed during rendering (-1)", exception.toString());
+ // pass
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testThread() throws Exception {
+ final AtomicBoolean failedUnexpectedly = new AtomicBoolean(false);
+ Thread otherThread = new Thread("other") {
+ @Override
+ public void run() {
+ try {
+ assertNull(RenderSecurityManager.getCurrent());
+ System.getProperties();
+ } catch (SecurityException e) {
+ failedUnexpectedly.set(true);
+ }
+ }
+ };
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ // Threads cloned from this one should inherit the same security constraints
+ final AtomicBoolean failedAsExpected = new AtomicBoolean(false);
+ final Thread renderThread = new Thread("render") {
+ @Override
+ public void run() {
+ try {
+ System.getProperties();
+ } catch (SecurityException e) {
+ failedAsExpected.set(true);
+ }
+ }
+ };
+ renderThread.start();
+ renderThread.join();
+ assertTrue(failedAsExpected.get());
+ otherThread.start();
+ otherThread.join();
+ assertFalse(failedUnexpectedly.get());
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testActive() throws Exception {
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ try {
+ manager.setActive(true);
+
+ try {
+ System.getProperties();
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ // pass
+ }
+
+ manager.setActive(false);
+
+ try {
+ System.getProperties();
+ } catch (SecurityException exception) {
+ fail(exception.toString());
+ }
+
+ manager.setActive(true);
+
+ try {
+ System.getProperties();
+ fail("Should have thrown security exception");
+ } catch (SecurityException exception) {
+ // pass
+ }
+ } finally {
+ manager.dispose();
+ }
+ }
+
+ public void testThread2() throws Exception {
+ // Check that when a new thread is created simultaneously from an unrelated
+ // thread during rendering, that new thread does not pick up the security manager.
+ //
+ final CyclicBarrier barrier1 = new CyclicBarrier(2);
+ final CyclicBarrier barrier2 = new CyclicBarrier(2);
+ final CyclicBarrier barrier3 = new CyclicBarrier(4);
+ final CyclicBarrier barrier4 = new CyclicBarrier(4);
+ final CyclicBarrier barrier5 = new CyclicBarrier(4);
+ final CyclicBarrier barrier6 = new CyclicBarrier(4);
+
+ // First the threads reach barrier1. Then from barrier1 to barrier2, thread1
+ // installs the security manager. Then from barrier2 to barrier3, thread2
+ // checks that it does not have any security restrictions, and creates thread3.
+ // Thread1 will ensure that the security manager is working there, and it will
+ // create thread4. Then after barrier3 (where thread3 and thread4 are now also
+ // participating) thread3 will ensure that it too has no security restrictions,
+ // and thread4 will ensure that it does. At barrier4 the security manager gets
+ // uninstalled, and at barrier5 all threads will check that there are no more
+ // restrictions. At barrier6 all threads are done.
+
+ final Thread thread1 = new Thread("render") {
+ @Override
+ public void run() {
+ try {
+ barrier1.await();
+ assertNull(RenderSecurityManager.getCurrent());
+
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ manager.setActive(true);
+
+ barrier2.await();
+
+ Thread thread4 = new Thread() {
+ @Override
+ public void run() {
+ try {
+ barrier3.await();
+
+ try {
+ System.getProperties();
+ fail("Should have thrown security exception");
+ } catch (SecurityException e) {
+ // pass
+ }
+
+ barrier4.await();
+ barrier5.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+ barrier6.await();
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ }
+ }
+ };
+ thread4.start();
+
+ try {
+ System.getProperties();
+ fail("Should have thrown security exception");
+ } catch (SecurityException e) {
+ // expected
+ }
+
+ barrier3.await();
+ barrier4.await();
+ manager.dispose();
+
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+
+ barrier5.await();
+ barrier6.await();
+
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ }
+
+ }
+ };
+
+ final Thread thread2 = new Thread("unrelated") {
+ @Override
+ public void run() {
+ try {
+ barrier1.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ barrier2.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNotNull(System.getSecurityManager());
+
+ try {
+ System.getProperties();
+ } catch (SecurityException e) {
+ fail("Should not have been affected by security manager");
+ }
+
+ Thread thread3 = new Thread() {
+ @Override
+ public void run() {
+ try {
+ barrier3.await();
+
+ try {
+ System.getProperties();
+ } catch (SecurityException e) {
+ fail("Should not have been affected by security manager");
+ }
+
+ barrier4.await();
+ barrier5.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+ barrier6.await();
+
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ }
+ }
+ };
+ thread3.start();
+
+ barrier3.await();
+ barrier4.await();
+ barrier5.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+ barrier6.await();
+
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ }
+
+ }
+ };
+
+ thread1.start();
+ thread2.start();
+ thread1.join();
+ thread2.join();
+ }
+
+ public void testDisabled() throws Exception {
+ assertNull(RenderSecurityManager.getCurrent());
+
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ RenderSecurityManager.sEnabled = false;
+ try {
+ assertNull(RenderSecurityManager.getCurrent());
+ manager.setActive(true);
+ assertSame(manager, RenderSecurityManager.getCurrent());
+ if (new File("/bin/ls").exists()) {
+ Runtime.getRuntime().exec("/bin/ls");
+ } else {
+ manager.checkExec("/bin/ls");
+ }
+ } catch (SecurityException exception) {
+ fail("Should have been disabled");
+ } finally {
+ RenderSecurityManager.sEnabled = true;
+ manager.dispose();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNull(System.getSecurityManager());
+ }
+ }
+
+ public void testLogger() throws Exception {
+ assertNull(RenderSecurityManager.getCurrent());
+
+ final CyclicBarrier barrier1 = new CyclicBarrier(2);
+ final CyclicBarrier barrier2 = new CyclicBarrier(2);
+ final CyclicBarrier barrier3 = new CyclicBarrier(2);
+
+ Thread thread = new Thread() {
+ @Override
+ public void run() {
+ try {
+ barrier1.await();
+ barrier2.await();
+
+ System.setSecurityManager(new SecurityManager() {
+ @Override
+ public String toString() {
+ return "MyTestSecurityManager";
+ }
+
+ @Override
+ public void checkPermission(Permission permission) {
+ }
+ });
+
+ barrier3.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNotNull(System.getSecurityManager());
+ assertEquals("MyTestSecurityManager", System.getSecurityManager().toString());
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ }
+ }
+ };
+ thread.start();
+
+ RenderSecurityManager manager = new RenderSecurityManager(null, null);
+ RecordingLogger logger = new RecordingLogger();
+ manager.setLogger(logger);
+ try {
+ barrier1.await();
+ assertNull(RenderSecurityManager.getCurrent());
+ manager.setActive(true);
+ assertSame(manager, RenderSecurityManager.getCurrent());
+ barrier2.await();
+ barrier3.await();
+
+ assertNull(RenderSecurityManager.getCurrent());
+ manager.setActive(false);
+ assertNull(RenderSecurityManager.getCurrent());
+
+ assertEquals(Collections.singletonList(
+ "RenderSecurityManager being replaced by another thread"),
+ logger.getWarningMsgs());
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ } catch (BrokenBarrierException e) {
+ fail(e.toString());
+ } finally {
+ manager.dispose();
+ assertNull(RenderSecurityManager.getCurrent());
+ assertNotNull(System.getSecurityManager());
+ assertEquals("MyTestSecurityManager", System.getSecurityManager().toString());
+ System.setSecurityManager(null);
+ }
+ }
+}
diff --git a/sdk-common/src/test/java/com/android/ide/common/res2/ResourceMergerTest.java b/sdk-common/src/test/java/com/android/ide/common/res2/ResourceMergerTest.java
index cb9bafc..8a64280 100755
--- a/sdk-common/src/test/java/com/android/ide/common/res2/ResourceMergerTest.java
+++ b/sdk-common/src/test/java/com/android/ide/common/res2/ResourceMergerTest.java
@@ -49,7 +49,7 @@
public void testMergeByCount() throws Exception {
ResourceMerger merger = getResourceMerger();
- assertEquals(27, merger.size());
+ assertEquals(28, merger.size());
}
public void testMergedResourcesByName() throws Exception {
diff --git a/sdk-common/src/test/java/com/android/ide/common/res2/ResourceSetTest.java b/sdk-common/src/test/java/com/android/ide/common/res2/ResourceSetTest.java
index d7f60be..df38801 100644
--- a/sdk-common/src/test/java/com/android/ide/common/res2/ResourceSetTest.java
+++ b/sdk-common/src/test/java/com/android/ide/common/res2/ResourceSetTest.java
@@ -27,7 +27,7 @@
public void testBaseResourceSetByCount() throws Exception {
ResourceSet resourceSet = getBaseResourceSet();
- assertEquals(25, resourceSet.size());
+ assertEquals(26, resourceSet.size());
}
public void testBaseResourceSetByName() throws Exception {
@@ -58,7 +58,8 @@
"declare-styleable/declare_styleable",
"dimen/dimen",
"id/item_id",
- "integer/integer"
+ "integer/integer",
+ "plurals/plurals"
);
}
diff --git a/sdk-common/src/test/java/com/android/ide/common/res2/ValueResourceParser2Test.java b/sdk-common/src/test/java/com/android/ide/common/res2/ValueResourceParser2Test.java
index 0d26985..887c144 100644
--- a/sdk-common/src/test/java/com/android/ide/common/res2/ValueResourceParser2Test.java
+++ b/sdk-common/src/test/java/com/android/ide/common/res2/ValueResourceParser2Test.java
@@ -32,7 +32,7 @@
public void testParsedResourcesByCount() throws Exception {
List<ResourceItem> resources = getParsedResources();
- assertEquals(20, resources.size());
+ assertEquals(21, resources.size());
}
public void testParsedResourcesByName() throws Exception {
@@ -61,7 +61,8 @@
"dimen/dimen",
"id/item_id",
"integer/integer",
- "layout/layout_ref"
+ "layout/layout_ref",
+ "plurals/plurals"
};
for (String name : resourceNames) {
diff --git a/sdk-common/src/test/java/com/android/ide/common/resources/ResourceResolverTest.java b/sdk-common/src/test/java/com/android/ide/common/resources/ResourceResolverTest.java
index adfe1b7..29616a4 100644
--- a/sdk-common/src/test/java/com/android/ide/common/resources/ResourceResolverTest.java
+++ b/sdk-common/src/test/java/com/android/ide/common/resources/ResourceResolverTest.java
@@ -9,6 +9,7 @@
import junit.framework.TestCase;
+import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -231,8 +232,17 @@
resolver.resolveValue(ResourceType.STRING, "bright_foreground_dark",
"@android:color/background_light", true).getValue());
+ // themeExtends
+ assertTrue(resolver.themeExtends("@android:style/Theme", "@android:style/Theme"));
+ assertTrue(resolver.themeExtends("@android:style/Theme", "@android:style/Theme.Light"));
+ assertFalse(resolver.themeExtends("@android:style/Theme.Light", "@android:style/Theme"));
+ assertTrue(resolver.themeExtends("@style/MyTheme.Dotted2", "@style/MyTheme.Dotted2"));
+ assertTrue(resolver.themeExtends("@style/MyTheme", "@style/MyTheme.Dotted2"));
+ assertTrue(resolver.themeExtends("@android:style/Theme.Light", "@style/MyTheme.Dotted2"));
+ assertTrue(resolver.themeExtends("@android:style/Theme", "@style/MyTheme.Dotted2"));
+ assertFalse(resolver.themeExtends("@style/MyTheme.Dotted1", "@style/MyTheme.Dotted2"));
- // Switch to MyTheme.Dotted1 (to make sure the parent="" inheritence works properly.)
+ // Switch to MyTheme.Dotted1 (to make sure the parent="" inheritance works properly.)
// To do that we need to create a new resource resolver.
resolver = ResourceResolver.create(projectResources, frameworkResources,
"MyTheme.Dotted1", true);
@@ -358,4 +368,67 @@
projectRepository.dispose();
}
+
+ public void testParentCycle() throws IOException {
+ TestResourceRepository projectRepository = TestResourceRepository.create(false,
+ new Object[]{
+ "values/styles.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<resources>\n"
+ + " <style name=\"ButtonStyle.Base\">\n"
+ + " <item name=\"android:textColor\">#ff0000</item>\n"
+ + " </style>\n"
+ + " <style name=\"ButtonStyle\" parent=\"ButtonStyle.Base\">\n"
+ + " <item name=\"android:layout_height\">40dp</item>\n"
+ + " </style>\n"
+ + "</resources>\n",
+
+ "layouts/layout.xml", ""
+ + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ + " android:layout_width=\"match_parent\"\n"
+ + " android:layout_height=\"match_parent\">\n"
+ + "\n"
+ + " <TextView\n"
+ + " style=\"@style/ButtonStyle\"\n"
+ + " android:layout_width=\"wrap_content\"\n"
+ + " android:layout_height=\"wrap_content\" />\n"
+ + "\n"
+ + "</RelativeLayout>\n",
+
+ });
+ assertFalse(projectRepository.isFrameworkRepository());
+ FolderConfiguration config = FolderConfiguration.getConfigForFolder("values-es-land");
+ assertNotNull(config);
+ Map<ResourceType, Map<String, ResourceValue>> projectResources =
+ projectRepository.getConfiguredResources(config);
+ assertNotNull(projectResources);
+ ResourceResolver resolver = ResourceResolver.create(projectResources, projectResources,
+ "ButtonStyle", true);
+ assertNotNull(resolver);
+
+ final AtomicBoolean wasWarned = new AtomicBoolean(false);
+ LayoutLog logger = new LayoutLog() {
+ @Override
+ public void error(String tag, String message, Object data) {
+ assertEquals("Cyclic style parent definitions: \"ButtonStyle\" specifies "
+ + "parent \"ButtonStyle.Base\" implies parent \"ButtonStyle\"", message);
+ assertEquals(LayoutLog.TAG_BROKEN, tag);
+ wasWarned.set(true);
+ }
+ };
+ resolver.setLogger(logger);
+
+ StyleResourceValue buttonStyle = (StyleResourceValue) resolver.findResValue(
+ "@style/ButtonStyle", false);
+ ResourceValue textColor = resolver.findItemInStyle(buttonStyle, "textColor", true);
+ assertNotNull(textColor);
+ assertEquals("#ff0000", textColor.getValue());
+ assertFalse(wasWarned.get());
+ ResourceValue missing = resolver.findItemInStyle(buttonStyle, "missing", true);
+ assertNull(missing);
+ assertTrue(wasWarned.get());
+
+ projectRepository.dispose();
+ }
}
diff --git a/sdk-common/src/test/java/com/android/ide/common/resources/configuration/FolderConfigurationTest.java b/sdk-common/src/test/java/com/android/ide/common/resources/configuration/FolderConfigurationTest.java
index 3f850ab..085f0ff 100644
--- a/sdk-common/src/test/java/com/android/ide/common/resources/configuration/FolderConfigurationTest.java
+++ b/sdk-common/src/test/java/com/android/ide/common/resources/configuration/FolderConfigurationTest.java
@@ -16,6 +16,7 @@
package com.android.ide.common.resources.configuration;
+import com.android.resources.ResourceFolderType;
import junit.framework.TestCase;
import java.util.ArrayList;
@@ -99,6 +100,15 @@
assertNull(configForFolder.getLayoutDirectionQualifier());
}
+ public void testToStrings() {
+ FolderConfiguration configForFolder = FolderConfiguration.getConfigForFolder("values-en-rUS");
+ assertNotNull(configForFolder);
+ assertEquals("Locale Language en_Region US", configForFolder.toDisplayString());
+ assertEquals("en,US", configForFolder.toShortDisplayString());
+ assertEquals("layout-en-rUS", configForFolder.getFolderName(ResourceFolderType.LAYOUT));
+ assertEquals("-en-rUS", configForFolder.getUniqueKey());
+ }
+
// --- helper methods
private final static class MockConfigurable implements Configurable {
diff --git a/sdk-common/src/test/resources/testData/resources/baseSet/values/values.xml b/sdk-common/src/test/resources/testData/resources/baseSet/values/values.xml
index 168813e..c3cdf71 100644
--- a/sdk-common/src/test/resources/testData/resources/baseSet/values/values.xml
+++ b/sdk-common/src/test/resources/testData/resources/baseSet/values/values.xml
@@ -82,4 +82,7 @@
<item type="layout" name="layout_ref">@layout/ref</item>
<item type="layout" name="alias_replaced_by_file">@layout/ref</item>
+ <plurals name="plurals">
+ <item quantity="one">test2 <xliff:g xmlns="urn:oasis:names:tc:xliff:document:1.2" id="test3">%s</xliff:g> test4</item>
+ </plurals>
</resources>
diff --git a/sdklib/src/main/java/com/android/sdklib/SdkManager.java b/sdklib/src/main/java/com/android/sdklib/SdkManager.java
index 5169deb..5dd9ece 100644
--- a/sdklib/src/main/java/com/android/sdklib/SdkManager.java
+++ b/sdklib/src/main/java/com/android/sdklib/SdkManager.java
@@ -170,7 +170,10 @@
LocalPkgInfo info = pkgsInfos[i];
assert info instanceof LocalPlatformPkgInfo;
if (info instanceof LocalPlatformPkgInfo) {
- targets.add(((LocalPlatformPkgInfo) info).getAndroidTarget());
+ IAndroidTarget target = ((LocalPlatformPkgInfo) info).getAndroidTarget();
+ if (target != null) {
+ targets.add(target);
+ }
}
}
mCachedTargets = targets.toArray(new IAndroidTarget[targets.size()]);
diff --git a/sdklib/src/main/java/com/android/sdklib/devices/nexus.xml b/sdklib/src/main/java/com/android/sdklib/devices/nexus.xml
index f3452cb..24fbbf4 100644
--- a/sdklib/src/main/java/com/android/sdklib/devices/nexus.xml
+++ b/sdklib/src/main/java/com/android/sdklib/devices/nexus.xml
@@ -693,4 +693,104 @@
</d:state>
</d:device>
+ <d:device>
+ <d:name>Nexus 5</d:name>
+ <d:id>Nexus 5</d:id>
+ <d:manufacturer>Google</d:manufacturer>
+ <d:hardware>
+ <d:screen>
+ <d:screen-size>normal</d:screen-size>
+ <d:diagonal-length>4.95</d:diagonal-length>
+ <d:pixel-density>xxhdpi</d:pixel-density>
+ <d:screen-ratio>notlong</d:screen-ratio>
+ <d:dimensions>
+ <d:x-dimension>1080</d:x-dimension>
+ <d:y-dimension>1920</d:y-dimension>
+ </d:dimensions>
+ <d:xdpi>445</d:xdpi>
+ <d:ydpi>445</d:ydpi>
+ <d:touch>
+ <d:multitouch>jazz-hands</d:multitouch>
+ <d:mechanism>finger</d:mechanism>
+ <d:screen-type>capacitive</d:screen-type>
+ </d:touch>
+ </d:screen>
+ <d:networking>
+ Wifi
+ Bluetooth
+ NFC
+ </d:networking>
+ <d:sensors>
+ Accelerometer
+ Barometer
+ Compass
+ GPS
+ Gyroscope
+ LightSensor
+ ProximitySensor
+ </d:sensors>
+ <d:mic>true</d:mic>
+ <d:camera>
+ <d:location>back</d:location>
+ <d:autofocus>true</d:autofocus>
+ <d:flash>true</d:flash>
+ </d:camera>
+ <d:camera>
+ <d:location>front</d:location>
+ <d:autofocus>false</d:autofocus>
+ <d:flash>false</d:flash>
+ </d:camera>
+ <d:keyboard>nokeys</d:keyboard>
+ <d:nav>nonav</d:nav>
+ <d:ram unit="GiB">2</d:ram>
+ <d:buttons>soft</d:buttons>
+ <d:internal-storage unit="GiB">16</d:internal-storage>
+ <d:removable-storage unit="MiB"></d:removable-storage>
+ <d:cpu>Snapdragon 800 (MSM8974)</d:cpu>
+ <d:gpu>Adreno 330</d:gpu>
+ <d:abi>
+ armeabi-v7a
+ armeabi
+ </d:abi>
+ <d:dock></d:dock>
+ <d:power-type>battery</d:power-type>
+ </d:hardware>
+ <d:software>
+ <d:api-level>19</d:api-level>
+ <d:live-wallpaper-support>true</d:live-wallpaper-support>
+ <d:bluetooth-profiles></d:bluetooth-profiles>
+ <d:gl-version>3.0</d:gl-version>
+ <d:gl-extensions>
+ GL_AMD_compressed_ATC_texture GL_AMD_performance_monitor GL_AMD_program_binary_Z400
+ GL_EXT_debug_label GL_EXT_debug_marker GL_EXT_discard_framebuffer
+ GL_EXT_robustness GL_EXT_texture_format_BGRA8888 GL_EXT_texture_type_2_10_10_10_REV
+ GL_NV_fence GL_OES_compressed_ETC1_RGB8_texture GL_OES_depth_texture GL_OES_depth24
+ GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_element_index_uint
+ GL_OES_fbo_render_mipmap GL_OES_fragment_precision_high GL_OES_get_program_binary
+ GL_OES_packed_depth_stencil GL_OES_depth_texture_cube_map GL_OES_rgb8_rgba8
+ GL_OES_standard_derivatives GL_OES_texture_3D GL_OES_texture_float
+ GL_OES_texture_half_float GL_OES_texture_half_float_linear GL_OES_texture_npot
+ GL_OES_vertex_half_float GL_OES_vertex_type_10_10_10_2 GL_OES_vertex_array_object
+ GL_QCOM_alpha_test GL_QCOM_binning_control GL_QCOM_driver_control
+ GL_QCOM_perfmon_global_mode GL_QCOM_extended_get GL_QCOM_extended_get2
+ GL_QCOM_tiled_rendering GL_QCOM_writeonly_rendering GL_EXT_sRGB
+ GL_EXT_texture_filter_anisotropic GL_EXT_color_buffer_float
+ GL_EXT_color_buffer_half_float GL_EXT_disjoint_timer_query
+ </d:gl-extensions>
+ <d:status-bar>true</d:status-bar>
+ </d:software>
+ <d:state name="Portrait" default="true">
+ <d:description>The phone in portrait view</d:description>
+ <d:screen-orientation>port</d:screen-orientation>
+ <d:keyboard-state>keyssoft</d:keyboard-state>
+ <d:nav-state>nonav</d:nav-state>
+ </d:state>
+ <d:state name="Landscape">
+ <d:description>The phone in landscape view</d:description>
+ <d:screen-orientation>land</d:screen-orientation>
+ <d:keyboard-state>keyssoft</d:keyboard-state>
+ <d:nav-state>nonav</d:nav-state>
+ </d:state>
+ </d:device>
+
</d:devices>
diff --git a/sdklib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java b/sdklib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java
index 506eb49..2dd0a26 100644
--- a/sdklib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java
+++ b/sdklib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java
@@ -25,6 +25,7 @@
import sun.security.x509.AlgorithmId;
import sun.security.x509.X500Name;
+import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -161,7 +162,7 @@
*/
public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
throws IOException, NoSuchAlgorithmException {
- mOutputJar = new JarOutputStream(out);
+ mOutputJar = new JarOutputStream(new BufferedOutputStream(out));
mOutputJar.setLevel(9);
mKey = key;
mCertificate = certificate;
diff --git a/sdklib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java b/sdklib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java
index 5e7cad5..e578747 100644
--- a/sdklib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java
+++ b/sdklib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java
@@ -23,7 +23,6 @@
import com.android.io.IAbstractFile;
import com.android.io.IAbstractFolder;
import com.android.io.StreamException;
-import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import com.android.utils.ILogger;
import com.google.common.io.Closeables;
@@ -77,6 +76,7 @@
public static final String PROPERTY_RULES_PATH = "layoutrules.jars";
public static final String PROPERTY_SDK = "sdk.dir";
+ public static final String PROPERTY_NDK = "ndk.dir";
// LEGACY - Kept so that we can actually remove it from local.properties.
private static final String PROPERTY_SDK_LEGACY = "sdk-location";
diff --git a/templates/activities/BlankActivity/globals.xml.ftl b/templates/activities/BlankActivity/globals.xml.ftl
index a926954..0dca7d3 100644
--- a/templates/activities/BlankActivity/globals.xml.ftl
+++ b/templates/activities/BlankActivity/globals.xml.ftl
@@ -1,13 +1,13 @@
<?xml version="1.0"?>
<globals>
<global id="projectOut" value="." />
- <global id="manifestOut" value="src/main" />
- <global id="appCompat" value="${(minApi lt 14)?string('1','')}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="appCompat" value="${(minApiLevel lt 14)?string('1','')}" />
<!-- e.g. getSupportActionBar vs. getActionBar -->
- <global id="Support" value="${(minApi lt 14)?string('Support','')}" />
+ <global id="Support" value="${(minApiLevel lt 14)?string('Support','')}" />
<global id="hasViewPager" value="${(navType == 'pager' || navType == 'tabs')?string('1','')}" />
<global id="hasSections" value="${(navType == 'pager' || navType == 'tabs' || navType == 'spinner' || navType == 'drawer')?string('1','')}" />
- <global id="srcOut" value="src/main/java/${slashedPackageName(packageName)}" />
- <global id="resOut" value="src/main/res" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="menuName" value="${classToResource(activityClass)}" />
</globals>
diff --git a/templates/activities/BlankActivity/root/AndroidManifest.xml.ftl b/templates/activities/BlankActivity/root/AndroidManifest.xml.ftl
index b8ae72e..98d8d7b 100644
--- a/templates/activities/BlankActivity/root/AndroidManifest.xml.ftl
+++ b/templates/activities/BlankActivity/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <activity android:name=".${activityClass}"
+ <activity android:name="${packageName}.${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
<#else>
diff --git a/templates/activities/BlankActivity/root/res/layout/activity_drawer.xml.ftl b/templates/activities/BlankActivity/root/res/layout/activity_drawer.xml.ftl
index 6aed55f..c23b77d 100644
--- a/templates/activities/BlankActivity/root/res/layout/activity_drawer.xml.ftl
+++ b/templates/activities/BlankActivity/root/res/layout/activity_drawer.xml.ftl
@@ -5,7 +5,7 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${activityClass}">
+ tools:context="${packageName}.${activityClass}">
<!-- As the main content view, the view below consumes the entire
space available using match_parent in both dimensions. -->
diff --git a/templates/activities/BlankActivity/root/res/layout/activity_fragment_container.xml.ftl b/templates/activities/BlankActivity/root/res/layout/activity_fragment_container.xml.ftl
index 935d379..da3a7e4 100644
--- a/templates/activities/BlankActivity/root/res/layout/activity_fragment_container.xml.ftl
+++ b/templates/activities/BlankActivity/root/res/layout/activity_fragment_container.xml.ftl
@@ -3,5 +3,5 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${activityClass}"
+ tools:context="${packageName}.${activityClass}"
tools:ignore="MergeRootFrame" />
diff --git a/templates/activities/BlankActivity/root/res/layout/activity_pager.xml.ftl b/templates/activities/BlankActivity/root/res/layout/activity_pager.xml.ftl
index 2d72cb9..bdaf98c 100644
--- a/templates/activities/BlankActivity/root/res/layout/activity_pager.xml.ftl
+++ b/templates/activities/BlankActivity/root/res/layout/activity_pager.xml.ftl
@@ -3,4 +3,4 @@
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${activityClass}" />
+ tools:context="${packageName}.${activityClass}" />
diff --git a/templates/activities/BlankActivity/root/res/layout/fragment_simple.xml.ftl b/templates/activities/BlankActivity/root/res/layout/fragment_simple.xml.ftl
index 99bb4b4..db8cc12 100644
--- a/templates/activities/BlankActivity/root/res/layout/fragment_simple.xml.ftl
+++ b/templates/activities/BlankActivity/root/res/layout/fragment_simple.xml.ftl
@@ -6,7 +6,7 @@
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
- tools:context=".${activityClass}$PlaceholderFragment">
+ tools:context="${packageName}.${activityClass}$PlaceholderFragment">
<TextView
<#if hasSections?has_content>android:id="@+id/section_label"<#else>android:text="@string/hello_world"</#if>
diff --git a/templates/activities/BlankActivity/root/res/menu/main.xml.ftl b/templates/activities/BlankActivity/root/res/menu/main.xml.ftl
index 3f68e07..054a3dc 100644
--- a/templates/activities/BlankActivity/root/res/menu/main.xml.ftl
+++ b/templates/activities/BlankActivity/root/res/menu/main.xml.ftl
@@ -1,5 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"<#if appCompat?has_content>
- xmlns:app="http://schemas.android.com/apk/res-auto"</#if>>
+ xmlns:app="http://schemas.android.com/apk/res-auto"</#if>
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="${packageName}.${activityClass}" >
<#if navType == 'drawer'><item android:id="@+id/action_example"
android:title="@string/action_example"
${(appCompat?has_content)?string('app','android')}:showAsAction="withText|ifRoom" /></#if>
diff --git a/templates/activities/BlankActivity/root/src/app_package/NavigationDrawerFragment.java.ftl b/templates/activities/BlankActivity/root/src/app_package/NavigationDrawerFragment.java.ftl
index 50cbd04..3880362 100644
--- a/templates/activities/BlankActivity/root/src/app_package/NavigationDrawerFragment.java.ftl
+++ b/templates/activities/BlankActivity/root/src/app_package/NavigationDrawerFragment.java.ftl
@@ -77,7 +77,11 @@
// Select either the default item (0) or the last selected item.
selectItem(mCurrentSelectedPosition);
+ }
+ @Override
+ public void onActivityCreated (Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
// Indicate that this fragment would like to influence the set of actions in the action bar.
setHasOptionsMenu(true);
}
@@ -95,7 +99,7 @@
});
mDrawerListView.setAdapter(new ArrayAdapter<String>(
getActionBar().getThemedContext(),
- android.R.layout.simple_list_item_<#if minApi gte 11>activated_</#if>1,
+ android.R.layout.simple_list_item_<#if minApiLevel gte 11>activated_</#if>1,
android.R.id.text1,
new String[]{
getString(R.string.title_section1),
@@ -160,7 +164,7 @@
mUserLearnedDrawer = true;
SharedPreferences sp = PreferenceManager
.getDefaultSharedPreferences(getActivity());
- sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).${(minApi gte 9)?string('apply','commit')}();
+ sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).${(minApiLevel gte 9)?string('apply','commit')}();
}
getActivity().${(appCompat?has_content)?string('supportInvalidate','invalidate')}OptionsMenu(); // calls onPrepareOptionsMenu()
@@ -243,10 +247,9 @@
return true;
}
- switch (item.getItemId()) {
- case R.id.action_example:
- Toast.makeText(getActivity(), "Example action.", Toast.LENGTH_SHORT).show();
- return true;
+ if (item.getItemId() == R.id.action_example) {
+ Toast.makeText(getActivity(), "Example action.", Toast.LENGTH_SHORT).show();
+ return true;
}
return super.onOptionsItemSelected(item);
diff --git a/templates/activities/BlankActivity/root/src/app_package/include_options_menu.java.ftl b/templates/activities/BlankActivity/root/src/app_package/include_options_menu.java.ftl
index cf1470e..d99a3d2 100644
--- a/templates/activities/BlankActivity/root/src/app_package/include_options_menu.java.ftl
+++ b/templates/activities/BlankActivity/root/src/app_package/include_options_menu.java.ftl
@@ -20,9 +20,9 @@
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
- switch (item.getItemId()) {
- case R.id.action_settings:
- return true;
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/FullscreenActivity/globals.xml.ftl b/templates/activities/FullscreenActivity/globals.xml.ftl
index 6d73e17..d566fee 100644
--- a/templates/activities/FullscreenActivity/globals.xml.ftl
+++ b/templates/activities/FullscreenActivity/globals.xml.ftl
@@ -1,8 +1,8 @@
<?xml version="1.0"?>
<globals>
<global id="projectOut" value="." />
- <global id="manifestOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
- <global id="resOut" value="res" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="simpleName" value="${activityToLayout(activityClass)}" />
</globals>
diff --git a/templates/activities/FullscreenActivity/root/AndroidManifest.xml.ftl b/templates/activities/FullscreenActivity/root/AndroidManifest.xml.ftl
index b909732..266df2f 100644
--- a/templates/activities/FullscreenActivity/root/AndroidManifest.xml.ftl
+++ b/templates/activities/FullscreenActivity/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <activity android:name=".${activityClass}"
+ <activity android:name="${packageName}.${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
<#else>
diff --git a/templates/activities/FullscreenActivity/root/res/layout/activity_fullscreen.xml.ftl b/templates/activities/FullscreenActivity/root/res/layout/activity_fullscreen.xml.ftl
index 39f801a..000b639 100644
--- a/templates/activities/FullscreenActivity/root/res/layout/activity_fullscreen.xml.ftl
+++ b/templates/activities/FullscreenActivity/root/res/layout/activity_fullscreen.xml.ftl
@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#0099cc"
- tools:context=".${activityClass}">
+ tools:context="${packageName}.${activityClass}">
<!-- The primary full-screen view. This can be replaced with whatever view
is needed to present your content, e.g. VideoView, SurfaceView,
@@ -25,7 +25,7 @@
android:fitsSystemWindows="true">
<LinearLayout android:id="@+id/fullscreen_content_controls"
- style="?buttonBarStyle"
+ style="?metaButtonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
@@ -34,7 +34,7 @@
tools:ignore="UselessParent">
<Button android:id="@+id/dummy_button"
- style="?buttonBarButtonStyle"
+ style="?metaButtonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
diff --git a/templates/activities/FullscreenActivity/root/res/values-v11/styles.xml b/templates/activities/FullscreenActivity/root/res/values-v11/styles.xml
index feaeb70..f72515d 100644
--- a/templates/activities/FullscreenActivity/root/res/values-v11/styles.xml
+++ b/templates/activities/FullscreenActivity/root/res/values-v11/styles.xml
@@ -4,8 +4,8 @@
<item name="android:actionBarStyle">@style/FullscreenActionBarStyle</item>
<item name="android:windowActionBarOverlay">true</item>
<item name="android:windowBackground">@null</item>
- <item name="buttonBarStyle">?android:attr/buttonBarStyle</item>
- <item name="buttonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
+ <item name="metaButtonBarStyle">?android:attr/buttonBarStyle</item>
+ <item name="metaButtonBarButtonStyle">?android:attr/buttonBarButtonStyle</item>
</style>
<style name="FullscreenActionBarStyle" parent="android:Widget.Holo.ActionBar">
diff --git a/templates/activities/FullscreenActivity/root/res/values/attrs.xml b/templates/activities/FullscreenActivity/root/res/values/attrs.xml
index 2cf1a1a..7ce840e 100644
--- a/templates/activities/FullscreenActivity/root/res/values/attrs.xml
+++ b/templates/activities/FullscreenActivity/root/res/values/attrs.xml
@@ -5,8 +5,8 @@
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels. -->
<declare-styleable name="ButtonBarContainerTheme">
- <attr name="buttonBarStyle" format="reference" />
- <attr name="buttonBarButtonStyle" format="reference" />
+ <attr name="metaButtonBarStyle" format="reference" />
+ <attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>
diff --git a/templates/activities/FullscreenActivity/root/res/values/styles.xml b/templates/activities/FullscreenActivity/root/res/values/styles.xml
index 48bb968..e95ba03 100644
--- a/templates/activities/FullscreenActivity/root/res/values/styles.xml
+++ b/templates/activities/FullscreenActivity/root/res/values/styles.xml
@@ -3,8 +3,8 @@
<style name="FullscreenTheme" parent="android:Theme.NoTitleBar">
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@null</item>
- <item name="buttonBarStyle">@style/ButtonBar</item>
- <item name="buttonBarButtonStyle">@style/ButtonBarButton</item>
+ <item name="metaButtonBarStyle">@style/ButtonBar</item>
+ <item name="metaButtonBarButtonStyle">@style/ButtonBarButton</item>
</style>
<!-- Backward-compatible version of ?android:attr/buttonBarStyle -->
diff --git a/templates/activities/FullscreenActivity/root/src/app_package/FullscreenActivity.java.ftl b/templates/activities/FullscreenActivity/root/src/app_package/FullscreenActivity.java.ftl
index 4714244..9d6109b 100644
--- a/templates/activities/FullscreenActivity/root/src/app_package/FullscreenActivity.java.ftl
+++ b/templates/activities/FullscreenActivity/root/src/app_package/FullscreenActivity.java.ftl
@@ -145,19 +145,19 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // This ID represents the Home or Up button. In the case of this
- // activity, the Up button is shown. Use NavUtils to allow users
- // to navigate up one level in the application structure. For
- // more details, see the Navigation pattern on Android Design:
- //
- // http://developer.android.com/design/patterns/navigation.html#up-vs-back
- //
- // TODO: If Settings has multiple levels, Up should navigate up
- // that hierarchy.
- NavUtils.navigateUpFromSameTask(this);
- return true;
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. Use NavUtils to allow users
+ // to navigate up one level in the application structure. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+ //
+ // TODO: If Settings has multiple levels, Up should navigate up
+ // that hierarchy.
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/FullscreenActivity/template.xml b/templates/activities/FullscreenActivity/template.xml
index d2617fb..cf568ea 100644
--- a/templates/activities/FullscreenActivity/template.xml
+++ b/templates/activities/FullscreenActivity/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="3"
+ format="4"
+ revision="4"
name="Fullscreen Activity"
description="Creates a new activity that toggles the visibility of the system UI (status and navigation bars) and action bar upon user interaction."
minApi="4"
diff --git a/templates/activities/LoginActivity/globals.xml.ftl b/templates/activities/LoginActivity/globals.xml.ftl
index fbe8985..05c9aad 100644
--- a/templates/activities/LoginActivity/globals.xml.ftl
+++ b/templates/activities/LoginActivity/globals.xml.ftl
@@ -1,9 +1,9 @@
<?xml version="1.0"?>
<globals>
<global id="projectOut" value="." />
- <global id="manifestOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
- <global id="resOut" value="res" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="menuName" value="${classToResource(activityClass)}" />
<global id="simpleName" value="${activityToLayout(activityClass)}" />
</globals>
diff --git a/templates/activities/LoginActivity/root/AndroidManifest.xml.ftl b/templates/activities/LoginActivity/root/AndroidManifest.xml.ftl
index c5f02d2..4f35c79 100644
--- a/templates/activities/LoginActivity/root/AndroidManifest.xml.ftl
+++ b/templates/activities/LoginActivity/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <activity android:name=".${activityClass}"
+ <activity android:name="${packageName}.${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
<#else>
diff --git a/templates/activities/LoginActivity/root/res/layout/activity_login.xml.ftl b/templates/activities/LoginActivity/root/res/layout/activity_login.xml.ftl
index 9434e5b..9f0fb73 100644
--- a/templates/activities/LoginActivity/root/res/layout/activity_login.xml.ftl
+++ b/templates/activities/LoginActivity/root/res/layout/activity_login.xml.ftl
@@ -1,6 +1,6 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- tools:context=".${activityClass}">
+ tools:context="${packageName}.${activityClass}">
<!-- Login progress -->
<LinearLayout android:id="@+id/login_status"
diff --git a/templates/activities/LoginActivity/root/res/menu/activity_login.xml b/templates/activities/LoginActivity/root/res/menu/activity_login.xml
index 2965794..f39c9a3 100644
--- a/templates/activities/LoginActivity/root/res/menu/activity_login.xml
+++ b/templates/activities/LoginActivity/root/res/menu/activity_login.xml
@@ -1,4 +1,6 @@
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ tools:context="${packageName}.${activityClass}">
<item android:id="@+id/action_forgot_password"
android:title="@string/action_forgot_password"
android:showAsAction="never" />
diff --git a/templates/activities/LoginActivity/root/src/app_package/LoginActivity.java.ftl b/templates/activities/LoginActivity/root/src/app_package/LoginActivity.java.ftl
index 8defdc7..4f28c21 100644
--- a/templates/activities/LoginActivity/root/src/app_package/LoginActivity.java.ftl
+++ b/templates/activities/LoginActivity/root/src/app_package/LoginActivity.java.ftl
@@ -106,19 +106,19 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // This ID represents the Home or Up button. In the case of this
- // activity, the Up button is shown. Use NavUtils to allow users
- // to navigate up one level in the application structure. For
- // more details, see the Navigation pattern on Android Design:
- //
- // http://developer.android.com/design/patterns/navigation.html#up-vs-back
- //
- // TODO: If Settings has multiple levels, Up should navigate up
- // that hierarchy.
- NavUtils.navigateUpFromSameTask(this);
- return true;
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. Use NavUtils to allow users
+ // to navigate up one level in the application structure. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+ //
+ // TODO: If Settings has multiple levels, Up should navigate up
+ // that hierarchy.
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/LoginActivity/template.xml b/templates/activities/LoginActivity/template.xml
index 501f1ea..576c633 100644
--- a/templates/activities/LoginActivity/template.xml
+++ b/templates/activities/LoginActivity/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="3"
+ format="4"
+ revision="4"
name="Login Activity"
description="Creates a new login activity, allowing users to enter an email address and password to log in to or register with your application."
minApi="3"
diff --git a/templates/activities/MasterDetailFlow/globals.xml.ftl b/templates/activities/MasterDetailFlow/globals.xml.ftl
index 415d60e..0d23f55 100644
--- a/templates/activities/MasterDetailFlow/globals.xml.ftl
+++ b/templates/activities/MasterDetailFlow/globals.xml.ftl
@@ -1,9 +1,9 @@
<?xml version="1.0"?>
<globals>
<global id="projectOut" value="." />
- <global id="manifestOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
- <global id="resOut" value="res" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="CollectionName" value="${extractLetters(objectKind)}List" />
<global id="collection_name" value="${extractLetters(objectKind?lower_case)}_list" />
<global id="DetailName" value="${extractLetters(objectKind)}Detail" />
diff --git a/templates/activities/MasterDetailFlow/root/AndroidManifest.xml.ftl b/templates/activities/MasterDetailFlow/root/AndroidManifest.xml.ftl
index 4707bd6..34d0402 100644
--- a/templates/activities/MasterDetailFlow/root/AndroidManifest.xml.ftl
+++ b/templates/activities/MasterDetailFlow/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
- <activity android:name=".${CollectionName}Activity"
+ <activity android:name="${packageName}.${CollectionName}Activity"
<#if isNewProject>
android:label="@string/app_name"
<#else>
@@ -20,11 +20,11 @@
</#if>
</activity>
- <activity android:name=".${DetailName}Activity"
+ <activity android:name="${packageName}.${DetailName}Activity"
android:label="@string/title_${detail_name}"
<#if buildApi gte 16>android:parentActivityName=".${CollectionName}Activity"</#if>>
<meta-data android:name="android.support.PARENT_ACTIVITY"
- android:value=".${CollectionName}Activity" />
+ android:value="${packageName}.${CollectionName}Activity" />
</activity>
</application>
diff --git a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl
index ddc1ecc..02bf4f6 100644
--- a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl
+++ b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_detail.xml.ftl
@@ -3,5 +3,5 @@
android:id="@+id/${detail_name}_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${DetailName}Activity"
+ tools:context="${packageName}.${DetailName}Activity"
tools:ignore="MergeRootFrame" />
diff --git a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl
index 065cd42..e51a98e 100644
--- a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl
+++ b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_list.xml.ftl
@@ -6,5 +6,5 @@
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
- tools:context=".${CollectionName}Activity"
+ tools:context="${packageName}.${CollectionName}Activity"
tools:layout="@android:layout/list_content" />
diff --git a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl
index 575e9e6..1f2bd19 100644
--- a/templates/activities/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl
+++ b/templates/activities/MasterDetailFlow/root/res/layout/activity_content_twopane.xml.ftl
@@ -8,7 +8,7 @@
android:divider="?android:attr/dividerHorizontal"
android:orientation="horizontal"
android:showDividers="middle"
- tools:context=".${CollectionName}Activity">
+ tools:context="${packageName}.${CollectionName}Activity">
<!--
This layout is a two-pane layout for the ${objectKindPlural}
diff --git a/templates/activities/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl b/templates/activities/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl
index 808fc31..f685145 100644
--- a/templates/activities/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl
+++ b/templates/activities/MasterDetailFlow/root/res/layout/fragment_content_detail.xml.ftl
@@ -6,4 +6,4 @@
android:layout_height="match_parent"
android:padding="16dp"
android:textIsSelectable="true"
- tools:context=".${DetailName}Fragment" />
+ tools:context="${packageName}.${DetailName}Fragment" />
diff --git a/templates/activities/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl b/templates/activities/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl
index 2cc6054..79f0e90 100644
--- a/templates/activities/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl
+++ b/templates/activities/MasterDetailFlow/root/src/app_package/ContentDetailActivity.java.ftl
@@ -50,17 +50,17 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // This ID represents the Home or Up button. In the case of this
- // activity, the Up button is shown. Use NavUtils to allow users
- // to navigate up one level in the application structure. For
- // more details, see the Navigation pattern on Android Design:
- //
- // http://developer.android.com/design/patterns/navigation.html#up-vs-back
- //
- NavUtils.navigateUpTo(this, new Intent(this, ${CollectionName}Activity.class));
- return true;
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. Use NavUtils to allow users
+ // to navigate up one level in the application structure. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+ //
+ NavUtils.navigateUpTo(this, new Intent(this, ${CollectionName}Activity.class));
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl b/templates/activities/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl
index ae73f7d3..fe02fe9 100644
--- a/templates/activities/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl
+++ b/templates/activities/MasterDetailFlow/root/src/app_package/ContentListActivity.java.ftl
@@ -60,17 +60,17 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // This ID represents the Home or Up button. In the case of this
- // activity, the Up button is shown. Use NavUtils to allow users
- // to navigate up one level in the application structure. For
- // more details, see the Navigation pattern on Android Design:
- //
- // http://developer.android.com/design/patterns/navigation.html#up-vs-back
- //
- NavUtils.navigateUpFromSameTask(this);
- return true;
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. Use NavUtils to allow users
+ // to navigate up one level in the application structure. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+ //
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/MasterDetailFlow/template.xml b/templates/activities/MasterDetailFlow/template.xml
index 6762321..c2e2b6e 100644
--- a/templates/activities/MasterDetailFlow/template.xml
+++ b/templates/activities/MasterDetailFlow/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="3"
+ format="4"
+ revision="4"
name="Master/Detail Flow"
minApi="11"
description="Creates a new master/detail flow, allowing users to view a collection of objects as well as details for each object. This flow is presented using two columns on tablet-size screens and one column on handsets and smaller screens. This template creates two activities, a master fragment, and a detail fragment.">
diff --git a/templates/activities/SettingsActivity/globals.xml.ftl b/templates/activities/SettingsActivity/globals.xml.ftl
index 6d73e17..d566fee 100644
--- a/templates/activities/SettingsActivity/globals.xml.ftl
+++ b/templates/activities/SettingsActivity/globals.xml.ftl
@@ -1,8 +1,8 @@
<?xml version="1.0"?>
<globals>
<global id="projectOut" value="." />
- <global id="manifestOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
- <global id="resOut" value="res" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="simpleName" value="${activityToLayout(activityClass)}" />
</globals>
diff --git a/templates/activities/SettingsActivity/root/AndroidManifest.xml.ftl b/templates/activities/SettingsActivity/root/AndroidManifest.xml.ftl
index 9f78fcf..a638dd6 100644
--- a/templates/activities/SettingsActivity/root/AndroidManifest.xml.ftl
+++ b/templates/activities/SettingsActivity/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <activity android:name=".${activityClass}"
+ <activity android:name="${packageName}.${activityClass}"
<#if isNewProject>
android:label="@string/app_name"
<#else>
diff --git a/templates/activities/SettingsActivity/root/src/app_package/SettingsActivity.java.ftl b/templates/activities/SettingsActivity/root/src/app_package/SettingsActivity.java.ftl
index bf67aca..bf4610f 100644
--- a/templates/activities/SettingsActivity/root/src/app_package/SettingsActivity.java.ftl
+++ b/templates/activities/SettingsActivity/root/src/app_package/SettingsActivity.java.ftl
@@ -63,19 +63,19 @@
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case android.R.id.home:
- // This ID represents the Home or Up button. In the case of this
- // activity, the Up button is shown. Use NavUtils to allow users
- // to navigate up one level in the application structure. For
- // more details, see the Navigation pattern on Android Design:
- //
- // http://developer.android.com/design/patterns/navigation.html#up-vs-back
- //
- // TODO: If Settings has multiple levels, Up should navigate up
- // that hierarchy.
- NavUtils.navigateUpFromSameTask(this);
- return true;
+ int id = item.getItemId();
+ if (id == android.R.id.home) {
+ // This ID represents the Home or Up button. In the case of this
+ // activity, the Up button is shown. Use NavUtils to allow users
+ // to navigate up one level in the application structure. For
+ // more details, see the Navigation pattern on Android Design:
+ //
+ // http://developer.android.com/design/patterns/navigation.html#up-vs-back
+ //
+ // TODO: If Settings has multiple levels, Up should navigate up
+ // that hierarchy.
+ NavUtils.navigateUpFromSameTask(this);
+ return true;
}
return super.onOptionsItemSelected(item);
}
diff --git a/templates/activities/SettingsActivity/template.xml b/templates/activities/SettingsActivity/template.xml
index 959dc34..33f9413 100644
--- a/templates/activities/SettingsActivity/template.xml
+++ b/templates/activities/SettingsActivity/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="3"
+ format="4"
+ revision="4"
name="Settings Activity"
description="Creates a new application settings activity that presents alternative layouts on handset and tablet-size screens."
minApi="4"
diff --git a/templates/gradle-projects/NewAndroidApplication/globals.xml.ftl b/templates/gradle-projects/NewAndroidApplication/globals.xml.ftl
deleted file mode 100644
index b7cc47a..0000000
--- a/templates/gradle-projects/NewAndroidApplication/globals.xml.ftl
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0"?>
-<globals>
- <global id="topOut" value="." />
- <global id="projectOut" value="." />
- <global id="appCompat" value="${(minApi lt 14)?string('1','')}" />
- <global id="manifestOut" value="src/main" />
- <global id="srcOut" value="src/main/java/${slashedPackageName(packageName)}" />
- <global id="resOut" value="src/main/res" />
- <global id="mavenUrl" value="mavenCentral" />
- <global id="buildToolsVersion" value="18.0.1" />
- <global id="gradlePluginVersion" value="0.6.+" />
- <global id="v4SupportLibraryVersion" value="13.0.+" />
-</globals>
diff --git a/templates/gradle-projects/NewAndroidApplication/root/build.gradle.ftl b/templates/gradle-projects/NewAndroidApplication/root/build.gradle.ftl
deleted file mode 100644
index 4e249bd..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/build.gradle.ftl
+++ /dev/null
@@ -1,39 +0,0 @@
-buildscript {
- repositories {
-<#if mavenUrl == "mavenCentral">
- mavenCentral()
-<#else>
- maven { url '${mavenUrl}' }
-</#if>
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:${gradlePluginVersion}'
- }
-}
-apply plugin: 'android'
-
-repositories {
-<#if mavenUrl == "mavenCentral">
- mavenCentral()
-<#else>
- maven { url '${mavenUrl}' }
-</#if>
-}
-
-android {
- compileSdkVersion ${buildApi}
- buildToolsVersion "${buildToolsVersion}"
-
- defaultConfig {
- minSdkVersion ${minApi}
- targetSdkVersion ${targetApi}
- }
-}
-
-dependencies {
- <#if dependencyList?? >
- <#list dependencyList as dependency>
- compile '${dependency}'
- </#list>
- </#if>
-}
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png
deleted file mode 100644
index 96a442e..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-hdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 359047d..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-mdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png
deleted file mode 100644
index 71c6d76..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-xhdpi/ic_launcher.png
+++ /dev/null
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/values/strings.xml.ftl b/templates/gradle-projects/NewAndroidApplication/root/res/values/strings.xml.ftl
deleted file mode 100644
index ee03444..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/res/values/strings.xml.ftl
+++ /dev/null
@@ -1,3 +0,0 @@
-<resources>
- <string name="app_name">${escapeXmlString(appTitle)}</string>
-</resources>
diff --git a/templates/gradle-projects/NewAndroidApplication/root/settings.gradle.ftl b/templates/gradle-projects/NewAndroidApplication/root/settings.gradle.ftl
deleted file mode 100644
index b12004b..0000000
--- a/templates/gradle-projects/NewAndroidApplication/root/settings.gradle.ftl
+++ /dev/null
@@ -1 +0,0 @@
-include ':${projectName}'
diff --git a/templates/gradle-projects/NewAndroidApplication/template_new_project.png b/templates/gradle-projects/NewAndroidApplication/template_new_project.png
deleted file mode 100644
index 92e8556..0000000
--- a/templates/gradle-projects/NewAndroidApplication/template_new_project.png
+++ /dev/null
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidLibrary/globals.xml.ftl b/templates/gradle-projects/NewAndroidLibrary/globals.xml.ftl
deleted file mode 100644
index 9870768..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/globals.xml.ftl
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0"?>
-<globals>
- <global id="topOut" value="." />
- <global id="projectOut" value="." />
- <global id="manifestOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
- <global id="resOut" value="res" />
- <global id="mavenUrl" value="mavenCentral" />
- <global id="buildToolsVersion" value="${buildApi}" />
- <global id="gradlePluginVersion" value="1.0.+" />
- <global id="v4SupportLibraryVersion" value="13.0.+" />
-</globals>
diff --git a/templates/gradle-projects/NewAndroidLibrary/recipe.xml.ftl b/templates/gradle-projects/NewAndroidLibrary/recipe.xml.ftl
deleted file mode 100644
index 5333bda..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/recipe.xml.ftl
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0"?>
-<recipe>
- <merge from="settings.gradle.ftl"
- to="${escapeXmlAttribute(topOut)}/settings.gradle" />
- <instantiate from="build.gradle.ftl"
- to="${escapeXmlAttribute(projectOut)}/build.gradle" />
- <instantiate from="AndroidManifest.xml.ftl"
- to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
-
-<#if copyIcons>
- <copy from="res/drawable-hdpi"
- to="${escapeXmlAttribute(resOut)}/drawable-hdpi" />
- <copy from="res/drawable-mdpi"
- to="${escapeXmlAttribute(resOut)}/drawable-mdpi" />
- <copy from="res/drawable-xhdpi"
- to="${escapeXmlAttribute(resOut)}/drawable-xhdpi" />
-</#if>
-<#if makeIgnore>
- <copy from="gitignore"
- to="${escapeXmlAttribute(projectOut)}/.gitignore" />
-</#if>
- <instantiate from="res/values/styles.xml.ftl"
- to="${escapeXmlAttribute(resOut)}/values/styles.xml" />
-<#if buildApi gte 11 && baseTheme != "none">
- <instantiate from="res/values-v11/styles_hc.xml.ftl"
- to="${escapeXmlAttribute(resOut)}/values-v11/styles.xml" />
-</#if>
-<#if buildApi gte 14 && baseTheme?contains("darkactionbar")>
- <copy from="res/values-v14/styles_ics.xml"
- to="${escapeXmlAttribute(resOut)}/values-v14/styles.xml" />
-</#if>
-
- <instantiate from="res/values/strings.xml.ftl"
- to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
-</recipe>
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/AndroidManifest.xml.ftl b/templates/gradle-projects/NewAndroidLibrary/root/AndroidManifest.xml.ftl
deleted file mode 100644
index 390a9da..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/AndroidManifest.xml.ftl
+++ /dev/null
@@ -1,15 +0,0 @@
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="${packageName}"
- android:versionCode="1"
- android:versionName="1.0">
-
- <uses-sdk android:minSdkVersion="${minApi}" <#if buildApi gte 4>android:targetSdkVersion="${targetApi}" </#if>/>
-
- <application <#if minApiLevel gte 4 && buildApi gte 4>android:allowBackup="true"</#if>
- android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"<#if baseTheme != "none">
- android:theme="@style/AppTheme"</#if>>
-
- </application>
-
-</manifest>
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/build.gradle.ftl b/templates/gradle-projects/NewAndroidLibrary/root/build.gradle.ftl
deleted file mode 100644
index 03161bc..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/build.gradle.ftl
+++ /dev/null
@@ -1,34 +0,0 @@
-buildscript {
- repositories {
-<#if mavenUrl == "mavenCentral">
- mavenCentral()
-<#else>
- maven { url '${mavenUrl}' }
-</#if>
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:${gradlePluginVersion}'
- }
-}
-apply plugin: 'android-library'
-
-repositories {
-<#if mavenUrl == "mavenCentral">
- mavenCentral()
-<#else>
- maven { url '${mavenUrl}' }
-</#if>
-}
-
-android {
- compileSdkVersion ${buildApi}
- buildToolsVersion "${buildToolsVersion}"
-
- defaultConfig {
- minSdkVersion ${minApi}
- targetSdkVersion ${targetApi}
- }
-}
-
-dependencies {
-}
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/gitignore b/templates/gradle-projects/NewAndroidLibrary/root/gitignore
deleted file mode 100644
index 796b96d..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/build
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/values-v11/styles_hc.xml.ftl b/templates/gradle-projects/NewAndroidLibrary/root/res/values-v11/styles_hc.xml.ftl
deleted file mode 100644
index f8993c3..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/res/values-v11/styles_hc.xml.ftl
+++ /dev/null
@@ -1,11 +0,0 @@
-<resources>
-
- <!--
- Base application theme for API 11+. This theme completely replaces
- AppBaseTheme from res/values/styles.xml on API 11+ devices.
- -->
- <style name="AppBaseTheme" parent="android:Theme.Holo<#if baseTheme?contains("light")>.Light</#if>">
- <!-- API 11 theme customizations can go here. -->
- </style>
-
-</resources>
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/values-v14/styles_ics.xml b/templates/gradle-projects/NewAndroidLibrary/root/res/values-v14/styles_ics.xml
deleted file mode 100644
index a91fd03..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/res/values-v14/styles_ics.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<resources>
-
- <!--
- Base application theme for API 14+. This theme completely replaces
- AppBaseTheme from BOTH res/values/styles.xml and
- res/values-v11/styles.xml on API 14+ devices.
- -->
- <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
- <!-- API 14 theme customizations can go here. -->
- </style>
-
-</resources>
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/values/styles.xml.ftl b/templates/gradle-projects/NewAndroidLibrary/root/res/values/styles.xml.ftl
deleted file mode 100644
index 30fe5b5..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/root/res/values/styles.xml.ftl
+++ /dev/null
@@ -1,20 +0,0 @@
-<resources>
-
- <!--
- Base application theme, dependent on API level. This theme is replaced
- by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
- -->
- <style name="AppBaseTheme" parent="android:Theme<#if baseTheme?contains("light")>.Light</#if>">
- <!--
- Theme customizations available in newer API levels can go in
- res/values-vXX/styles.xml, while customizations related to
- backward-compatibility can go here.
- -->
- </style>
-
- <!-- Application theme. -->
- <style name="AppTheme" parent="AppBaseTheme">
- <!-- All customizations that are NOT specific to a particular API-level can go here. -->
- </style>
-
-</resources>
diff --git a/templates/gradle-projects/NewAndroidLibrary/template.xml b/templates/gradle-projects/NewAndroidLibrary/template.xml
deleted file mode 100644
index d6b0014..0000000
--- a/templates/gradle-projects/NewAndroidLibrary/template.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<?xml version="1.0"?>
-<template
- format="1"
- revision="3"
- name="Android Library"
- description="Creates a new Android library.">
-
- <thumbs>
- <thumb>template_new_project.png</thumb>
- </thumbs>
-
- <category value="Applications" />
-
- <parameter
- id="packageName"
- name="Package name"
- type="string"
- constraints="package|nonempty"
- default="com.mycompany.myapp" />
-
- <parameter
- id="appTitle"
- name="Library title"
- type="string"
- constraints="nonempty"
- default="My Library"/>
-
- <parameter
- id="baseTheme"
- name="Base Theme"
- type="enum"
- default="holo_light_darkactionbar"
- help="The base user interface theme for the library">
- <option id="none">None</option>
- <option id="holo_dark" minBuildApi="11">Holo Dark</option>
- <option id="holo_light" minBuildApi="11">Holo Light</option>
- <option id="holo_light_darkactionbar" minBuildApi="14" default="true">Holo Light with Dark Action Bar</option>
- </parameter>
-
- <parameter
- id="minApi"
- name="Minimum API level"
- type="string"
- constraints="apilevel"
- default="7" />
-
- <!--
- Usually the same as minApi, but when minApi is a code name this will be the corresponding
- API level
- -->
- <parameter
- id="minApiLevel"
- name="Minimum API level"
- type="string"
- constraints="apilevel"
- default="7" />
-
- <parameter
- id="targetApi"
- name="Target API level"
- type="string"
- constraints="apilevel"
- default="16" />
-
- <parameter
- id="buildApi"
- name="Build API level"
- type="string"
- constraints="apilevel"
- default="16" />
-
- <parameter
- id="copyIcons"
- name="Include launcher icons"
- type="boolean"
- default="true" />
-
- <parameter
- id="makeIgnore"
- name="Create .gitignore file"
- type="boolean"
- default="true" />
-
- <globals file="globals.xml.ftl" />
- <execute file="recipe.xml.ftl" />
-
-</template>
diff --git a/templates/gradle-projects/NewAndroidModule/globals.xml.ftl b/templates/gradle-projects/NewAndroidModule/globals.xml.ftl
new file mode 100644
index 0000000..1843c0d
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidModule/globals.xml.ftl
@@ -0,0 +1,13 @@
+<?xml version="1.0"?>
+<globals>
+ <global id="topOut" value="." />
+ <global id="projectOut" value="." />
+ <global id="appCompat" value="${(minApiLevel lt 14)?string('1','')}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="mavenUrl" value="mavenCentral" />
+ <global id="buildToolsVersion" value="18.0.1" />
+ <global id="gradlePluginVersion" value="0.6.+" />
+ <global id="v4SupportLibraryVersion" value="13.0.+" />
+</globals>
diff --git a/templates/gradle-projects/NewAndroidApplication/recipe.xml.ftl b/templates/gradle-projects/NewAndroidModule/recipe.xml.ftl
similarity index 82%
rename from templates/gradle-projects/NewAndroidApplication/recipe.xml.ftl
rename to templates/gradle-projects/NewAndroidModule/recipe.xml.ftl
index f3bd847..ade222a 100644
--- a/templates/gradle-projects/NewAndroidApplication/recipe.xml.ftl
+++ b/templates/gradle-projects/NewAndroidModule/recipe.xml.ftl
@@ -4,6 +4,10 @@
<#if appCompat?has_content><dependency mavenUrl="com.android.support:appcompat-v7:+"/></#if>
+<#if !createActivity>
+ <mkdir at="${srcOut}" />
+</#if>
+
<merge from="settings.gradle.ftl"
to="${escapeXmlAttribute(topOut)}/settings.gradle" />
<instantiate from="build.gradle.ftl"
@@ -22,13 +26,17 @@
to="${escapeXmlAttribute(resOut)}/drawable-xxhdpi" />
</#if>
<#if makeIgnore>
- <copy from="project_ignore"
- to="${escapeXmlAttribute(topOut)}/.gitignore" />
<copy from="module_ignore"
to="${escapeXmlAttribute(projectOut)}/.gitignore" />
</#if>
+<#if enableProGuard>
+ <instantiate from="proguard-rules.txt.ftl"
+ to="${escapeXmlAttribute(projectOut)}/proguard-rules.txt" />
+</#if>
+<#if !(isLibraryProject??) || !isLibraryProject>
<instantiate from="res/values/styles.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/styles.xml" />
+</#if>
<instantiate from="res/values/strings.xml.ftl"
to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
diff --git a/templates/gradle-projects/NewAndroidApplication/root/AndroidManifest.xml.ftl b/templates/gradle-projects/NewAndroidModule/root/AndroidManifest.xml.ftl
similarity index 95%
rename from templates/gradle-projects/NewAndroidApplication/root/AndroidManifest.xml.ftl
rename to templates/gradle-projects/NewAndroidModule/root/AndroidManifest.xml.ftl
index 390a9da..b086306 100644
--- a/templates/gradle-projects/NewAndroidApplication/root/AndroidManifest.xml.ftl
+++ b/templates/gradle-projects/NewAndroidModule/root/AndroidManifest.xml.ftl
@@ -7,7 +7,7 @@
<application <#if minApiLevel gte 4 && buildApi gte 4>android:allowBackup="true"</#if>
android:label="@string/app_name"
- android:icon="@drawable/ic_launcher"<#if baseTheme != "none">
+ android:icon="@drawable/ic_launcher"<#if baseTheme != "none" && !isLibraryProject>
android:theme="@style/AppTheme"</#if>>
</application>
diff --git a/templates/gradle-projects/NewAndroidModule/root/build.gradle.ftl b/templates/gradle-projects/NewAndroidModule/root/build.gradle.ftl
new file mode 100644
index 0000000..951fde0
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidModule/root/build.gradle.ftl
@@ -0,0 +1,71 @@
+buildscript {
+ repositories {
+<#if mavenUrl == "mavenCentral">
+ mavenCentral()
+<#else>
+ maven { url '${mavenUrl}' }
+</#if>
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:${gradlePluginVersion}'
+ }
+}
+<#if isLibraryProject?? && isLibraryProject>
+apply plugin: 'android-library'
+<#else>
+apply plugin: 'android'
+</#if>
+
+repositories {
+<#if mavenUrl == "mavenCentral">
+ mavenCentral()
+<#else>
+ maven { url '${mavenUrl}' }
+</#if>
+}
+
+android {
+ compileSdkVersion ${buildApi}
+ buildToolsVersion "${buildToolsVersion}"
+
+ defaultConfig {
+ minSdkVersion ${minApi}
+ targetSdkVersion ${targetApi}
+ }
+<#if javaVersion?? && javaVersion != "1.6">
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_${javaVersion?replace('.','_','i')}
+ targetCompatibility JavaVersion.VERSION_${javaVersion?replace('.','_','i')}
+ }
+</#if>
+<#if enableProGuard>
+ <#if isLibraryProject>
+ release {
+ runProguard false
+ proguardFile 'proguard-rules.txt'
+ proguardFile getDefaultProguardFile('proguard-android.txt')
+ }
+ <#else>
+ buildTypes {
+ release {
+ runProguard false
+ proguardFile getDefaultProguardFile('proguard-android.txt')
+ }
+ }
+ productFlavors {
+ defaultFlavor {
+ proguardFile 'proguard-rules.txt'
+ }
+ }
+ </#if>
+</#if>
+}
+
+dependencies {
+ <#if dependencyList?? >
+ <#list dependencyList as dependency>
+ compile '${dependency}'
+ </#list>
+ </#if>
+}
diff --git a/templates/gradle-projects/NewAndroidApplication/root/module_ignore b/templates/gradle-projects/NewAndroidModule/root/module_ignore
similarity index 100%
rename from templates/gradle-projects/NewAndroidApplication/root/module_ignore
rename to templates/gradle-projects/NewAndroidModule/root/module_ignore
diff --git a/templates/gradle-projects/NewAndroidModule/root/proguard-rules.txt.ftl b/templates/gradle-projects/NewAndroidModule/root/proguard-rules.txt.ftl
new file mode 100644
index 0000000..f766622
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidModule/root/proguard-rules.txt.ftl
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdkDir}/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}
\ No newline at end of file
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/drawable-hdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidModule/root/res/drawable-hdpi/ic_launcher.png
old mode 100755
new mode 100644
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/root/res/drawable-hdpi/ic_launcher.png
rename to templates/gradle-projects/NewAndroidModule/root/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/drawable-mdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidModule/root/res/drawable-mdpi/ic_launcher.png
old mode 100755
new mode 100644
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/root/res/drawable-mdpi/ic_launcher.png
rename to templates/gradle-projects/NewAndroidModule/root/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/drawable-xhdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidModule/root/res/drawable-xhdpi/ic_launcher.png
old mode 100755
new mode 100644
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/root/res/drawable-xhdpi/ic_launcher.png
rename to templates/gradle-projects/NewAndroidModule/root/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/drawable-xxhdpi/ic_launcher.png b/templates/gradle-projects/NewAndroidModule/root/res/drawable-xxhdpi/ic_launcher.png
similarity index 100%
rename from templates/gradle-projects/NewAndroidApplication/root/res/drawable-xxhdpi/ic_launcher.png
rename to templates/gradle-projects/NewAndroidModule/root/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/res/values/strings.xml.ftl b/templates/gradle-projects/NewAndroidModule/root/res/values/strings.xml.ftl
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/root/res/values/strings.xml.ftl
rename to templates/gradle-projects/NewAndroidModule/root/res/values/strings.xml.ftl
diff --git a/templates/gradle-projects/NewAndroidApplication/root/res/values/styles.xml.ftl b/templates/gradle-projects/NewAndroidModule/root/res/values/styles.xml.ftl
similarity index 100%
rename from templates/gradle-projects/NewAndroidApplication/root/res/values/styles.xml.ftl
rename to templates/gradle-projects/NewAndroidModule/root/res/values/styles.xml.ftl
diff --git a/templates/gradle-projects/NewAndroidLibrary/root/settings.gradle.ftl b/templates/gradle-projects/NewAndroidModule/root/settings.gradle.ftl
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/root/settings.gradle.ftl
rename to templates/gradle-projects/NewAndroidModule/root/settings.gradle.ftl
diff --git a/templates/gradle-projects/NewAndroidApplication/template.xml b/templates/gradle-projects/NewAndroidModule/template.xml
similarity index 83%
rename from templates/gradle-projects/NewAndroidApplication/template.xml
rename to templates/gradle-projects/NewAndroidModule/template.xml
index 68e397b..dc0ecfa 100644
--- a/templates/gradle-projects/NewAndroidApplication/template.xml
+++ b/templates/gradle-projects/NewAndroidModule/template.xml
@@ -2,8 +2,8 @@
<template
format="4"
revision="4"
- name="Android Application"
- description="Creates a new Android application.">
+ name="Android Module"
+ description="Creates a new Android module.">
<thumbs>
<thumb>template_new_project.png</thumb>
@@ -20,17 +20,18 @@
<parameter
id="appTitle"
- name="Application title"
+ name="Module title"
type="string"
constraints="nonempty"
- default="My Application" />
+ default="My Module" />
<parameter
id="baseTheme"
name="Base Theme"
type="enum"
default="holo_light_darkactionbar"
- help="The base user interface theme for the application">
+ help="The base user interface theme for the module">
+ <option id="none">None</option>
<option id="holo_dark" minBuildApi="11">Holo Dark</option>
<option id="holo_light" minBuildApi="11">Holo Light</option>
<option id="holo_light_darkactionbar" minBuildApi="14" default="true">Holo Light with Dark Action Bar</option>
@@ -80,6 +81,12 @@
type="boolean"
default="true" />
+ <parameter
+ id="enableProGuard"
+ name="Enable ProGuard"
+ type="boolean"
+ default="true" />
+
<globals file="globals.xml.ftl" />
<execute file="recipe.xml.ftl" />
diff --git a/templates/gradle-projects/NewAndroidLibrary/template_new_project.png b/templates/gradle-projects/NewAndroidModule/template_new_project.png
similarity index 100%
rename from templates/gradle-projects/NewAndroidLibrary/template_new_project.png
rename to templates/gradle-projects/NewAndroidModule/template_new_project.png
Binary files differ
diff --git a/templates/gradle-projects/NewAndroidProject/globals.xml.ftl b/templates/gradle-projects/NewAndroidProject/globals.xml.ftl
new file mode 100644
index 0000000..26e600e
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/globals.xml.ftl
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<globals>
+ <global id="topOut" value="." />
+</globals>
diff --git a/templates/gradle-projects/NewAndroidProject/recipe.xml.ftl b/templates/gradle-projects/NewAndroidProject/recipe.xml.ftl
new file mode 100644
index 0000000..5321458
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/recipe.xml.ftl
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<recipe>
+ <instantiate from="build.gradle.ftl"
+ to="${escapeXmlAttribute(topOut)}/build.gradle" />
+
+<#if makeIgnore>
+ <copy from="project_ignore"
+ to="${escapeXmlAttribute(topOut)}/.gitignore" />
+</#if>
+
+ <instantiate from="settings.gradle.ftl"
+ to="${escapeXmlAttribute(topOut)}/settings.gradle" />
+
+ <instantiate from="gradle.properties.ftl"
+ to="${escapeXmlAttribute(topOut)}/gradle.properties" />
+
+ <copy from="$TEMPLATEDIR/gradle/wrapper"
+ to="${escapeXmlAttribute(topOut)}/" />
+
+<#if sdkDir??>
+ <instantiate from="local.properties.ftl"
+ to="${escapeXmlAttribute(topOut)}/local.properties" />
+</#if>
+</recipe>
diff --git a/templates/gradle-projects/NewAndroidProject/root/build.gradle.ftl b/templates/gradle-projects/NewAndroidProject/root/build.gradle.ftl
new file mode 100644
index 0000000..f7a7ae7
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/root/build.gradle.ftl
@@ -0,0 +1 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
\ No newline at end of file
diff --git a/templates/gradle-projects/NewAndroidProject/root/gradle.properties.ftl b/templates/gradle-projects/NewAndroidProject/root/gradle.properties.ftl
new file mode 100644
index 0000000..5d08ba7
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/root/gradle.properties.ftl
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Settings specified in this file will override any Gradle settings
+# configured through the IDE.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/templates/gradle-projects/NewAndroidProject/root/local.properties.ftl b/templates/gradle-projects/NewAndroidProject/root/local.properties.ftl
new file mode 100644
index 0000000..9dcbf9b
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/root/local.properties.ftl
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=${escapePropertyValue(sdkDir)}
\ No newline at end of file
diff --git a/templates/gradle-projects/NewAndroidApplication/root/project_ignore b/templates/gradle-projects/NewAndroidProject/root/project_ignore
similarity index 100%
rename from templates/gradle-projects/NewAndroidApplication/root/project_ignore
rename to templates/gradle-projects/NewAndroidProject/root/project_ignore
diff --git a/templates/gradle-projects/NewAndroidProject/root/settings.gradle.ftl b/templates/gradle-projects/NewAndroidProject/root/settings.gradle.ftl
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/root/settings.gradle.ftl
diff --git a/templates/gradle-projects/NewAndroidProject/template.xml b/templates/gradle-projects/NewAndroidProject/template.xml
new file mode 100644
index 0000000..6ae554a
--- /dev/null
+++ b/templates/gradle-projects/NewAndroidProject/template.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<template
+ format="4"
+ revision="1"
+ name="Android Project"
+ description="Creates a new Android project.">
+
+ <thumbs>
+ <thumb>template_new_project.png</thumb>
+ </thumbs>
+
+ <category value="Projects" />
+
+ <parameter
+ id="makeIgnore"
+ name="Create .gitignore file"
+ type="boolean"
+ default="true" />
+
+ <globals file="globals.xml.ftl" />
+ <execute file="recipe.xml.ftl" />
+
+</template>
diff --git a/templates/gradle-projects/NewAndroidLibrary/template_new_project.png b/templates/gradle-projects/NewAndroidProject/template_new_project.png
similarity index 100%
copy from templates/gradle-projects/NewAndroidLibrary/template_new_project.png
copy to templates/gradle-projects/NewAndroidProject/template_new_project.png
Binary files differ
diff --git a/templates/gradle-projects/NewJavaLibrary/globals.xml.ftl b/templates/gradle-projects/NewJavaLibrary/globals.xml.ftl
index b731454..8d19d26 100644
--- a/templates/gradle-projects/NewJavaLibrary/globals.xml.ftl
+++ b/templates/gradle-projects/NewJavaLibrary/globals.xml.ftl
@@ -2,5 +2,5 @@
<globals>
<global id="topOut" value="." />
<global id="projectOut" value="." />
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/gradle-projects/NewJavaLibrary/template.xml b/templates/gradle-projects/NewJavaLibrary/template.xml
index 5d6c526..a9f35c1 100644
--- a/templates/gradle-projects/NewJavaLibrary/template.xml
+++ b/templates/gradle-projects/NewJavaLibrary/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="1"
- revision="3"
+ format="4"
+ revision="4"
name="Java Library"
description="Creates a new Java library.">
diff --git a/templates/gradle/wrapper/gradle/wrapper/gradle-wrapper.properties b/templates/gradle/wrapper/gradle/wrapper/gradle-wrapper.properties
index 5c22dec..861eddc 100644
--- a/templates/gradle/wrapper/gradle/wrapper/gradle-wrapper.properties
+++ b/templates/gradle/wrapper/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=http\://services.gradle.org/distributions/gradle-1.6-bin.zip
+distributionUrl=http\://services.gradle.org/distributions/gradle-1.8-bin.zip
diff --git a/templates/other/AppWidget/globals.xml.ftl b/templates/other/AppWidget/globals.xml.ftl
index ac85374..65e6905 100644
--- a/templates/other/AppWidget/globals.xml.ftl
+++ b/templates/other/AppWidget/globals.xml.ftl
@@ -1,5 +1,7 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
<global id="class_name" value="${camelCaseToUnderscore(className)}" />
</globals>
diff --git a/templates/other/AppWidget/recipe.xml.ftl b/templates/other/AppWidget/recipe.xml.ftl
index 876b7b0..9db22d2 100644
--- a/templates/other/AppWidget/recipe.xml.ftl
+++ b/templates/other/AppWidget/recipe.xml.ftl
@@ -1,31 +1,36 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
- <copy from="res/drawable-nodpi/example_appwidget_preview.png" />
+ <copy from="res/drawable-nodpi/example_appwidget_preview.png"
+ to="${escapeXmlAttribute(resOut)}/drawable-nodpi/example_appwidget_preview.png" />
<instantiate from="res/layout/appwidget.xml"
- to="res/layout/${class_name}.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/${class_name}.xml" />
<#if configurable>
<instantiate from="res/layout/appwidget_configure.xml"
- to="res/layout/${class_name}_configure.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/${class_name}_configure.xml" />
</#if>
<instantiate from="res/xml/appwidget_info.xml.ftl"
- to="res/xml/${class_name}_info.xml" />
- <merge from="res/values/strings.xml.ftl" />
- <merge from="res/values-v14/dimens.xml" />
- <merge from="res/values/dimens.xml" />
+ to="${escapeXmlAttribute(resOut)}/xml/${class_name}_info.xml" />
+ <merge from="res/values/strings.xml.ftl"
+ to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
+ <merge from="res/values-v14/dimens.xml"
+ to="${escapeXmlAttribute(resOut)}/values-v14/dimens.xml" />
+ <merge from="res/values/dimens.xml"
+ to="${escapeXmlAttribute(resOut)}/values/dimens.xml" />
<instantiate from="src/app_package/AppWidget.java.ftl"
- to="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
<#if configurable>
<instantiate from="src/app_package/AppWidgetConfigureActivity.java.ftl"
- to="${srcOut}/${className}ConfigureActivity.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}ConfigureActivity.java" />
</#if>
- <open file="${srcOut}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/AppWidget/root/AndroidManifest.xml.ftl b/templates/other/AppWidget/root/AndroidManifest.xml.ftl
index 8b96d56..bf8f820 100644
--- a/templates/other/AppWidget/root/AndroidManifest.xml.ftl
+++ b/templates/other/AppWidget/root/AndroidManifest.xml.ftl
@@ -3,7 +3,7 @@
<application>
- <receiver android:name=".${className}" >
+ <receiver android:name="${packageName}.${className}" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
@@ -14,7 +14,7 @@
</receiver>
<#if configurable>
- <activity android:name=".${className}ConfigureActivity" >
+ <activity android:name="${packageName}.${className}ConfigureActivity" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
diff --git a/templates/other/AppWidget/template.xml b/templates/other/AppWidget/template.xml
index f071363..d79ef0c 100644
--- a/templates/other/AppWidget/template.xml
+++ b/templates/other/AppWidget/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New App Widget"
description="Creates a new App Widget"
minApi="4"
diff --git a/templates/other/BlankFragment/globals.xml.ftl b/templates/other/BlankFragment/globals.xml.ftl
index bfc27eb..6e28120 100644
--- a/templates/other/BlankFragment/globals.xml.ftl
+++ b/templates/other/BlankFragment/globals.xml.ftl
@@ -1,4 +1,5 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/other/BlankFragment/recipe.xml.ftl b/templates/other/BlankFragment/recipe.xml.ftl
index add6d27..0008b9b 100644
--- a/templates/other/BlankFragment/recipe.xml.ftl
+++ b/templates/other/BlankFragment/recipe.xml.ftl
@@ -1,18 +1,19 @@
<?xml version="1.0"?>
<recipe>
- <merge from="res/values/strings.xml" />
+ <dependency mavenUrl="com.android.support:support-v4:+"/>
+ <merge from="res/values/strings.xml" to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
<#if includeLayout>
<instantiate from="res/layout/fragment_blank.xml.ftl"
- to="res/layout/fragment_${classToResource(className)}.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/fragment_${classToResource(className)}.xml" />
- <open file="res/layout/fragment_${classToResource(className)}.xml" />
+ <open file="${escapeXmlAttribute(resOut)}/layout/fragment_${classToResource(className)}.xml" />
</#if>
- <open file="${srcOut}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
<instantiate from="src/app_package/BlankFragment.java.ftl"
- to="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/BlankFragment/root/res/layout/fragment_blank.xml.ftl b/templates/other/BlankFragment/root/res/layout/fragment_blank.xml.ftl
index 3e04f05..5712aae 100644
--- a/templates/other/BlankFragment/root/res/layout/fragment_blank.xml.ftl
+++ b/templates/other/BlankFragment/root/res/layout/fragment_blank.xml.ftl
@@ -2,7 +2,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${className}">
+ tools:context="${packageName}.${className}">
<!-- TODO: Update blank fragment layout -->
<TextView
diff --git a/templates/other/BlankFragment/template.xml b/templates/other/BlankFragment/template.xml
index 9434c18..5ed7c07 100644
--- a/templates/other/BlankFragment/template.xml
+++ b/templates/other/BlankFragment/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New Blank Fragment"
description="Creates a blank fragment that is compatible back to API level 4."
minApi="7"
diff --git a/templates/other/BroadcastReceiver/globals.xml.ftl b/templates/other/BroadcastReceiver/globals.xml.ftl
index bfc27eb..7beccc1 100644
--- a/templates/other/BroadcastReceiver/globals.xml.ftl
+++ b/templates/other/BroadcastReceiver/globals.xml.ftl
@@ -1,4 +1,5 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/other/BroadcastReceiver/recipe.xml.ftl b/templates/other/BroadcastReceiver/recipe.xml.ftl
index a9d2623..d889414 100644
--- a/templates/other/BroadcastReceiver/recipe.xml.ftl
+++ b/templates/other/BroadcastReceiver/recipe.xml.ftl
@@ -1,7 +1,8 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<instantiate from="src/app_package/BroadcastReceiver.java.ftl"
- to="${srcOut}/${className}.java" />
- <open file="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/BroadcastReceiver/root/AndroidManifest.xml.ftl b/templates/other/BroadcastReceiver/root/AndroidManifest.xml.ftl
index 27b60b0..6849a3b 100644
--- a/templates/other/BroadcastReceiver/root/AndroidManifest.xml.ftl
+++ b/templates/other/BroadcastReceiver/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <receiver android:name=".${className}"
+ <receiver android:name="${packageName}.${className}"
android:exported="${isExported?string}"
android:enabled="${isEnabled?string}" >
</receiver>
diff --git a/templates/other/BroadcastReceiver/template.xml b/templates/other/BroadcastReceiver/template.xml
index b17851e..159ff75 100644
--- a/templates/other/BroadcastReceiver/template.xml
+++ b/templates/other/BroadcastReceiver/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="1"
- revision="1"
+ format="4"
+ revision="2"
name="Broadcast Receiver"
description="Creates a new broadcast receiver component and adds it to your Android manifest.">
diff --git a/templates/other/ContentProvider/globals.xml.ftl b/templates/other/ContentProvider/globals.xml.ftl
index bfc27eb..7beccc1 100644
--- a/templates/other/ContentProvider/globals.xml.ftl
+++ b/templates/other/ContentProvider/globals.xml.ftl
@@ -1,4 +1,5 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/other/ContentProvider/recipe.xml.ftl b/templates/other/ContentProvider/recipe.xml.ftl
index 6cdad82..9bc3e61 100644
--- a/templates/other/ContentProvider/recipe.xml.ftl
+++ b/templates/other/ContentProvider/recipe.xml.ftl
@@ -1,7 +1,8 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<instantiate from="src/app_package/ContentProvider.java.ftl"
- to="${srcOut}/${className}.java" />
- <open file="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/ContentProvider/root/AndroidManifest.xml.ftl b/templates/other/ContentProvider/root/AndroidManifest.xml.ftl
index 6fa4afc..819925d 100644
--- a/templates/other/ContentProvider/root/AndroidManifest.xml.ftl
+++ b/templates/other/ContentProvider/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <provider android:name=".${className}"
+ <provider android:name="${packageName}.${className}"
android:authorities="${authorities}"
android:exported="${isExported?string}"
android:enabled="${isEnabled?string}" >
diff --git a/templates/other/ContentProvider/template.xml b/templates/other/ContentProvider/template.xml
index 21ed1be..dc7e1ea 100644
--- a/templates/other/ContentProvider/template.xml
+++ b/templates/other/ContentProvider/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="1"
- revision="1"
+ format="4"
+ revision="2"
name="Content Provider"
description="Creates a new content provider component and adds it to your Android manifest.">
diff --git a/templates/other/CustomView/globals.xml.ftl b/templates/other/CustomView/globals.xml.ftl
index d2eeb40..58fe1d0 100644
--- a/templates/other/CustomView/globals.xml.ftl
+++ b/templates/other/CustomView/globals.xml.ftl
@@ -1,5 +1,6 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="view_class" value="${camelCaseToUnderscore(viewClass)}" />
</globals>
diff --git a/templates/other/CustomView/recipe.xml.ftl b/templates/other/CustomView/recipe.xml.ftl
index d152df0..ce605b2 100644
--- a/templates/other/CustomView/recipe.xml.ftl
+++ b/templates/other/CustomView/recipe.xml.ftl
@@ -1,13 +1,13 @@
<?xml version="1.0"?>
<recipe>
<merge from="res/values/attrs.xml.ftl"
- to="res/values/attrs_${view_class}.xml" />
+ to="${escapeXmlAttribute(resOut)}/values/attrs_${view_class}.xml" />
<instantiate from="res/layout/sample.xml.ftl"
- to="res/layout/sample_${view_class}.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/sample_${view_class}.xml" />
<instantiate from="src/app_package/CustomView.java.ftl"
- to="${srcOut}/${viewClass}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${viewClass}.java" />
- <open file="${srcOut}/${viewClass}.java" />
- <open file="res/layout/sample_${view_class}.xml" />
+ <open file="${escapeXmlAttribute(srcOut)}/${viewClass}.java" />
+ <open file="${escapeXmlAttribute(resOut)}/layout/sample_${view_class}.xml" />
</recipe>
diff --git a/templates/other/CustomView/root/res/layout/sample.xml.ftl b/templates/other/CustomView/root/res/layout/sample.xml.ftl
index 03125b2..7dc4232 100755
--- a/templates/other/CustomView/root/res/layout/sample.xml.ftl
+++ b/templates/other/CustomView/root/res/layout/sample.xml.ftl
@@ -1,6 +1,6 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
-<#if !isLibraryProject>
+<#if !isLibraryProject && (!(isGradle??) || !isGradle) >
xmlns:app="http://schemas.android.com/apk/res/${packageName}"
<#else>
xmlns:app="http://schemas.android.com/apk/res-auto"
diff --git a/templates/other/CustomView/template.xml b/templates/other/CustomView/template.xml
index 55f6f3d..5373030 100644
--- a/templates/other/CustomView/template.xml
+++ b/templates/other/CustomView/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="1"
- revision="1"
+ format="4"
+ revision="2"
name="Custom View"
description="Creates a new custom view that extends android.view.View and exposes custom attributes.">
diff --git a/templates/other/Daydream/globals.xml.ftl b/templates/other/Daydream/globals.xml.ftl
index 04537f9..4d64b36 100644
--- a/templates/other/Daydream/globals.xml.ftl
+++ b/templates/other/Daydream/globals.xml.ftl
@@ -1,6 +1,8 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="class_name" value="${classToResource(className)}" />
<global id="info_name" value="${classToResource(className)}_info" />
<global id="settingsClassName" value="${className}SettingsActivity" />
diff --git a/templates/other/Daydream/recipe.xml.ftl b/templates/other/Daydream/recipe.xml.ftl
index 8db9811..5b99917 100644
--- a/templates/other/Daydream/recipe.xml.ftl
+++ b/templates/other/Daydream/recipe.xml.ftl
@@ -1,26 +1,28 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
- <merge from="res/values/strings.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
+ <merge from="res/values/strings.xml.ftl"
+ to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
<copy from="res/layout-v17/dream.xml"
- to="res/layout-v17/${class_name}.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout-v17/${class_name}.xml" />
<instantiate from="src/app_package/DreamService.java.ftl"
- to="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
<#if configurable>
<copy from="res/xml/dream_prefs.xml"
- to="res/xml/${prefs_name}.xml" />
+ to="${escapeXmlAttribute(resOut)}/xml/${prefs_name}.xml" />
<instantiate from="src/app_package/SettingsActivity.java.ftl"
- to="${srcOut}/${settingsClassName}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${settingsClassName}.java" />
<instantiate from="res/xml/xml_dream.xml.ftl"
- to="res/xml/${info_name}.xml" />
+ to="${escapeXmlAttribute(resOut)}/xml/${info_name}.xml" />
</#if>
- <open file="${srcOut}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/Daydream/root/AndroidManifest.xml.ftl b/templates/other/Daydream/root/AndroidManifest.xml.ftl
index c23bc6e..01beaec 100644
--- a/templates/other/Daydream/root/AndroidManifest.xml.ftl
+++ b/templates/other/Daydream/root/AndroidManifest.xml.ftl
@@ -4,12 +4,12 @@
<#if configurable>
<activity
- android:name=".${settingsClassName}" />
+ android:name="${packageName}.${settingsClassName}" />
</#if>
<!-- This service is only used on devices with API v17+ -->
<service
- android:name=".${className}"
+ android:name="${packageName}.${className}"
android:exported="true" >
<intent-filter>
<action android:name="android.service.dreams.DreamService" />
diff --git a/templates/other/Daydream/template.xml b/templates/other/Daydream/template.xml
index 2014eee..cd292a4 100644
--- a/templates/other/Daydream/template.xml
+++ b/templates/other/Daydream/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New Daydream"
description="Creates a new Daydream service component, for use on devices running Android 4.2 and later."
minBuildApi="17">
diff --git a/templates/other/IntentService/globals.xml.ftl b/templates/other/IntentService/globals.xml.ftl
index bfc27eb..b44b9f0 100644
--- a/templates/other/IntentService/globals.xml.ftl
+++ b/templates/other/IntentService/globals.xml.ftl
@@ -1,4 +1,4 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/other/IntentService/recipe.xml.ftl b/templates/other/IntentService/recipe.xml.ftl
index 958f6fd..197fc1f 100644
--- a/templates/other/IntentService/recipe.xml.ftl
+++ b/templates/other/IntentService/recipe.xml.ftl
@@ -1,7 +1,8 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<instantiate from="src/app_package/IntentService.java.ftl"
- to="${srcOut}/${className}.java" />
- <open file="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/IntentService/root/AndroidManifest.xml.ftl b/templates/other/IntentService/root/AndroidManifest.xml.ftl
index fcc0b66..61b66bb 100644
--- a/templates/other/IntentService/root/AndroidManifest.xml.ftl
+++ b/templates/other/IntentService/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <service android:name=".${className}"
+ <service android:name="${packageName}.${className}"
android:exported="false" >
</service>
</application>
diff --git a/templates/other/IntentService/template.xml b/templates/other/IntentService/template.xml
index ffe6cd5..3d86c9c 100644
--- a/templates/other/IntentService/template.xml
+++ b/templates/other/IntentService/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New IntentService"
description="Creates a new intent service class."
minApi="3"
diff --git a/templates/other/ListFragment/globals.xml.ftl b/templates/other/ListFragment/globals.xml.ftl
index 577250d..29dbb98 100644
--- a/templates/other/ListFragment/globals.xml.ftl
+++ b/templates/other/ListFragment/globals.xml.ftl
@@ -1,6 +1,7 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="collection_name" value="${extractLetters(objectKind?lower_case)}" />
<global id="className" value="${extractLetters(objectKind)}Fragment" />
<global id="fragment_layout" value="fragment_${extractLetters(objectKind?lower_case)}" />
diff --git a/templates/other/ListFragment/recipe.xml.ftl b/templates/other/ListFragment/recipe.xml.ftl
index 5db4fdb..7be60e9 100644
--- a/templates/other/ListFragment/recipe.xml.ftl
+++ b/templates/other/ListFragment/recipe.xml.ftl
@@ -1,26 +1,28 @@
<?xml version="1.0"?>
<recipe>
+ <dependency mavenUrl="com.android.support:support-v4:+"/>
<#if switchGrid == true>
- <merge from="res/values/refs.xml.ftl" />
+ <merge from="res/values/refs.xml.ftl"
+ to="${escapeXmlAttribute(resOut)}/values/refs.xml" />
<merge from="res/values/refs_lrg.xml.ftl"
- to="res/values-large/refs.xml" />
+ to="${escapeXmlAttribute(resOut)}/values-large/refs.xml" />
<merge from="res/values/refs_lrg.xml.ftl"
- to="res/values-sw600dp/refs.xml" />
+ to="${escapeXmlAttribute(resOut)}/values-sw600dp/refs.xml" />
<instantiate from="res/layout/fragment_grid.xml"
- to="res/layout/${fragment_layout}_grid.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/${fragment_layout}_grid.xml" />
<instantiate from="res/layout/fragment_list.xml"
- to="res/layout/${fragment_layout}_list.xml" />
+ to="${escapeXmlAttribute(resOut)}/layout/${fragment_layout}_list.xml" />
</#if>
<instantiate from="src/app_package/ListFragment.java.ftl"
- to="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
<instantiate from="src/app_package/dummy/DummyContent.java.ftl"
- to="${srcOut}/dummy/DummyContent.java" />
+ to="${escapeXmlAttribute(srcOut)}/dummy/DummyContent.java" />
- <open file="${srcOut}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/ListFragment/root/res/layout/fragment_grid.xml b/templates/other/ListFragment/root/res/layout/fragment_grid.xml
index 87ae969..55c0044 100644
--- a/templates/other/ListFragment/root/res/layout/fragment_grid.xml
+++ b/templates/other/ListFragment/root/res/layout/fragment_grid.xml
@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${className}">
+ tools:context="${packageName}.${className}">
<GridView
android:id="@android:id/list"
diff --git a/templates/other/ListFragment/root/res/layout/fragment_list.xml b/templates/other/ListFragment/root/res/layout/fragment_list.xml
index 5b0e178..e37a0a6 100644
--- a/templates/other/ListFragment/root/res/layout/fragment_list.xml
+++ b/templates/other/ListFragment/root/res/layout/fragment_list.xml
@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".${className}">
+ tools:context="${packageName}.${className}">
<ListView
android:id="@android:id/list"
diff --git a/templates/other/ListFragment/template.xml b/templates/other/ListFragment/template.xml
index e00257c..81eabad 100644
--- a/templates/other/ListFragment/template.xml
+++ b/templates/other/ListFragment/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New List Fragment"
description="Creates a new empty fragment containing a list that can optionally change to a grid when on large screens. Compatible back to API level 4."
minApi="7"
diff --git a/templates/other/Notification/globals.xml.ftl b/templates/other/Notification/globals.xml.ftl
index b302aa9..93d7472 100644
--- a/templates/other/Notification/globals.xml.ftl
+++ b/templates/other/Notification/globals.xml.ftl
@@ -1,9 +1,10 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="resOut" value="${resDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
<global id="notificationName" value="${className?replace('notification','','i')}" />
<global id="notification_name" value="${camelCaseToUnderscore(className?replace('notification','','i'))}" />
<global id="display_title" value="${camelCaseToUnderscore(className?replace('notification','','i'))?replace('_',' ')?cap_first}" />
-
<global id="icon_resource" value="ic_stat_${camelCaseToUnderscore(className?replace('notification','','i'))}" />
</globals>
diff --git a/templates/other/Notification/recipe.xml.ftl b/templates/other/Notification/recipe.xml.ftl
index bd1c265..1f1afec 100644
--- a/templates/other/Notification/recipe.xml.ftl
+++ b/templates/other/Notification/recipe.xml.ftl
@@ -1,25 +1,31 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <dependency mavenUrl="com.android.support:support-v4:+"/>
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<#if expandedStyle == "picture">
<copy from="res/drawable-nodpi/example_picture_large.png"
- to="res/drawable-nodpi/example_picture.png" />
+ to="${escapeXmlAttribute(resOut)}/drawable-nodpi/example_picture.png" />
<#else>
<copy from="res/drawable-nodpi/example_picture_small.png"
- to="res/drawable-nodpi/example_picture.png" />
+ to="${escapeXmlAttribute(resOut)}/drawable-nodpi/example_picture.png" />
</#if>
<#if moreActions>
- <copy from="res/drawable-hdpi" />
- <copy from="res/drawable-mdpi" />
- <copy from="res/drawable-xhdpi" />
+ <copy from="res/drawable-hdpi"
+ to="${escapeXmlAttribute(resOut)}/drawable-hdpi" />
+ <copy from="res/drawable-mdpi"
+ to="${escapeXmlAttribute(resOut)}/drawable-mdpi" />
+ <copy from="res/drawable-xhdpi"
+ to="${escapeXmlAttribute(resOut)}/drawable-xhdpi" />
</#if>
- <merge from="res/values/strings.xml.ftl" />
+ <merge from="res/values/strings.xml.ftl"
+ to="${escapeXmlAttribute(resOut)}/values/strings.xml" />
<instantiate from="src/app_package/NotificationHelper.java.ftl"
- to="${srcOut}/${className}.java" />
- <open file="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/Notification/template.xml b/templates/other/Notification/template.xml
index 61fbc59..b9479b2 100644
--- a/templates/other/Notification/template.xml
+++ b/templates/other/Notification/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="3"
- revision="1"
+ format="4"
+ revision="2"
name="New Notification"
description="Creates a new helper class that can show or cancel a status bar notification."
minApi="4">
diff --git a/templates/other/Service/globals.xml.ftl b/templates/other/Service/globals.xml.ftl
index bfc27eb..7beccc1 100644
--- a/templates/other/Service/globals.xml.ftl
+++ b/templates/other/Service/globals.xml.ftl
@@ -1,4 +1,5 @@
<?xml version="1.0"?>
<globals>
- <global id="srcOut" value="src/${slashedPackageName(packageName)}" />
+ <global id="manifestOut" value="${manifestDir}" />
+ <global id="srcOut" value="${srcDir}/${slashedPackageName(packageName)}" />
</globals>
diff --git a/templates/other/Service/recipe.xml.ftl b/templates/other/Service/recipe.xml.ftl
index 6e4bd57..9e66da5 100644
--- a/templates/other/Service/recipe.xml.ftl
+++ b/templates/other/Service/recipe.xml.ftl
@@ -1,7 +1,8 @@
<?xml version="1.0"?>
<recipe>
- <merge from="AndroidManifest.xml.ftl" />
+ <merge from="AndroidManifest.xml.ftl"
+ to="${escapeXmlAttribute(manifestOut)}/AndroidManifest.xml" />
<instantiate from="src/app_package/Service.java.ftl"
- to="${srcOut}/${className}.java" />
- <open file="${srcOut}/${className}.java" />
+ to="${escapeXmlAttribute(srcOut)}/${className}.java" />
+ <open file="${escapeXmlAttribute(srcOut)}/${className}.java" />
</recipe>
diff --git a/templates/other/Service/root/AndroidManifest.xml.ftl b/templates/other/Service/root/AndroidManifest.xml.ftl
index 14b0bce..0f747ea 100644
--- a/templates/other/Service/root/AndroidManifest.xml.ftl
+++ b/templates/other/Service/root/AndroidManifest.xml.ftl
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
<application>
- <service android:name=".${className}"
+ <service android:name="${packageName}.${className}"
android:exported="${isExported?string}"
android:enabled="${isEnabled?string}" >
</service>
diff --git a/templates/other/Service/template.xml b/templates/other/Service/template.xml
index 481fe74..de5b36c 100644
--- a/templates/other/Service/template.xml
+++ b/templates/other/Service/template.xml
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<template
- format="1"
- revision="1"
+ format="4"
+ revision="2"
name="Service"
description="Creates a new service component and adds it to your Android manifest.">