Add support to build and bundle library projects.
Xavier Ducrohet [Wed, 29 Aug 2012 00:31:49 +0000 (17:31 -0700)]
The unarchived bundle is saved under build/bundle.

The generated manifest is directly saved under there
to avoid copying it.
The resources are copied there but the built-type
res folder is not properly merged in there. Need
to fix this.
The crunched resources are copied on top of it.

Also add a library test app.

Change-Id: Iab2d437b6f98ca5f842ca9720aafa206691fc1f0

27 files changed:
.gitignore
builder/.gitignore
builder/src/main/java/com/android/builder/AndroidBuilder.java
gradle/src/main/groovy/com/android/build/gradle/AndroidBasePlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/AndroidExtension.groovy
gradle/src/main/groovy/com/android/build/gradle/AndroidLibraryPlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/AndroidPlugin.groovy
gradle/src/main/groovy/com/android/build/gradle/BaseAndroidExtension.groovy
testapps/tictactoe/README.txt [new file with mode: 0644]
testapps/tictactoe/app/build.gradle [new file with mode: 0644]
testapps/tictactoe/app/src/main/AndroidManifest.xml [new file with mode: 0755]
testapps/tictactoe/app/src/main/java/com/example/android/tictactoe/MainActivity.java [new file with mode: 0755]
testapps/tictactoe/app/src/main/res/drawable/icon.png [new file with mode: 0755]
testapps/tictactoe/app/src/main/res/layout/main.xml [new file with mode: 0755]
testapps/tictactoe/app/src/main/res/values/strings.xml [new file with mode: 0755]
testapps/tictactoe/build.gradle [new file with mode: 0644]
testapps/tictactoe/lib/build.gradle [new file with mode: 0644]
testapps/tictactoe/lib/src/main/AndroidManifest.xml [new file with mode: 0755]
testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameActivity.java [new file with mode: 0755]
testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameView.java [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/drawable/lib_bg.9.png [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/drawable/lib_circle.png [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/drawable/lib_cross.png [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/layout-land/lib_game.xml [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/layout/lib_game.xml [new file with mode: 0755]
testapps/tictactoe/lib/src/main/res/values/strings.xml [new file with mode: 0755]
testapps/tictactoe/settings.gradle [new file with mode: 0644]

index 2c9c85e..c6fbd72 100644 (file)
@@ -7,5 +7,6 @@ local.properties
 gradle/build
 testapps/*/build
 testapps/multiproject/*/build
+testapps/tictactoe/*/build
 /repo
 /out
index 7466a16..18a73cd 100644 (file)
@@ -1,10 +1,10 @@
-.gradle
-build
-builder.iml
-builder.ipr
-builder.iws
-.classpath
-.project
-bin
+/.gradle
+/build
+/builder.iml
+/builder.ipr
+/builder.iws
+/.classpath
+/.project
+/bin
 
 
index 48bbba4..6b32aea 100644 (file)
@@ -118,6 +118,9 @@ public class AndroidBuilder {
      * @see IAndroidTarget#hashString()
      */
     public void setTarget(@NonNull String target) {
+        if (target == null) {
+            throw new RuntimeException("Compilation target not set!");
+        }
         mTarget = mSdkParser.resolveTarget(target, mLogger);
 
         if (mTarget == null) {
@@ -486,7 +489,7 @@ public class AndroidBuilder {
             command.add(sourceOutputDir);
         }
 
-        if (resPackageOutput != null) {
+        if (mVariant.getType() != VariantConfiguration.Type.LIBRARY && resPackageOutput != null) {
             command.add("-F");
             command.add(resPackageOutput);
 
index d0f49ed..3f220cd 100644 (file)
@@ -35,6 +35,8 @@ import org.gradle.api.tasks.compile.Compile
  */
 abstract class AndroidBasePlugin {
 
+    public final static String INSTALL_GROUP = "Install"
+
     private final Map<Object, AndroidBuilder> builders = [:]
 
     protected Project project
@@ -57,6 +59,8 @@ abstract class AndroidBasePlugin {
 
         project.tasks.assemble.description =
             "Assembles all variants of all applications and secondary packages."
+
+        findSdk(project)
     }
 
     protected setDefaultConfig(ProductFlavor defaultConfig) {
@@ -96,7 +100,7 @@ abstract class AndroidBasePlugin {
         builders.put(key, androidBuilder)
     }
 
-    protected void findSdk(Project project) {
+    private void findSdk(Project project) {
         def localProperties = project.file("local.properties")
         if (localProperties.exists()) {
             Properties properties = new Properties()
@@ -132,13 +136,19 @@ abstract class AndroidBasePlugin {
         return androidBuilder.runtimeClasspath.join(":")
     }
 
+    /**
+     * Returns the folder directly under build/ into which the generated manifest is saved.
+     */
+    protected abstract String getManifestOutDir();
+
     protected ProcessManifest createProcessManifestTask(ApplicationVariant variant) {
         def processManifestTask = project.tasks.add("process${variant.name}Manifest",
                 ProcessManifest)
         processManifestTask.plugin = this
         processManifestTask.variant = variant
         processManifestTask.conventionMapping.mergedManifest = {
-            project.file("$project.buildDir/manifests/$variant.dirName/AndroidManifest.xml")
+            project.file(
+                    "$project.buildDir/${getManifestOutDir()}/$variant.dirName/AndroidManifest.xml")
         }
         return processManifestTask
     }
index 3b7ccc7..ce37122 100644 (file)
@@ -15,8 +15,6 @@
  */
 package com.android.build.gradle
 
-import com.android.build.gradle.internal.AaptOptionsImpl
-import com.android.build.gradle.internal.DexOptionsImpl
 import com.android.builder.BuildType
 import com.android.builder.ProductFlavor
 import org.gradle.api.Action
@@ -29,12 +27,8 @@ class AndroidExtension extends BaseAndroidExtension {
 
     String testBuildType = "debug"
 
-    final AaptOptionsImpl aaptOptions = new AaptOptionsImpl()
-    final DexOptionsImpl dexOptions = new DexOptionsImpl()
-
     AndroidExtension(NamedDomainObjectContainer<BuildType> buildTypes,
                      NamedDomainObjectContainer<ProductFlavor> productFlavors) {
-        super()
         this.buildTypes = buildTypes
         this.productFlavors = productFlavors
     }
@@ -46,12 +40,4 @@ class AndroidExtension extends BaseAndroidExtension {
     void productFlavors(Action<? super NamedDomainObjectContainer<ProductFlavor>> action) {
         action.execute(productFlavors)
     }
-
-    void aaptOptions(Action<AaptOptionsImpl> action) {
-        action.execute(aaptOptions)
-    }
-
-    void dexOptions(Action<DexOptionsImpl> action) {
-        action.execute(dexOptions)
-    }
 }
index fb65ecc..56eeb1f 100644 (file)
  */
 package com.android.build.gradle
 
+import com.android.build.gradle.internal.BuildTypeData
+import com.android.build.gradle.internal.ProductFlavorData
+import com.android.build.gradle.internal.ProductionAppVariant
 import com.android.builder.BuildType
+import com.android.builder.VariantConfiguration
 import org.gradle.api.Plugin
 import org.gradle.api.Project
-import com.android.build.gradle.internal.BuildTypeData
-import com.android.builder.VariantConfiguration
-import com.android.build.gradle.internal.ProductionAppVariant
-import com.android.build.gradle.internal.ProductFlavorData
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.bundling.Jar
+import org.gradle.api.tasks.bundling.Zip
 
 class AndroidLibraryPlugin extends AndroidBasePlugin implements Plugin<Project> {
 
+    private final static String DIR_BUNDLES = "bundles";
+
     AndroidLibraryExtension extension
     BuildTypeData debugBuildTypeData
     BuildTypeData releaseBuildTypeData
@@ -50,15 +55,16 @@ class AndroidLibraryPlugin extends AndroidBasePlugin implements Plugin<Project>
     }
 
     void createAndroidTasks() {
-        createLibraryTasks()
+        createLibraryTasks(debugBuildTypeData)
+        createLibraryTasks(releaseBuildTypeData)
     }
 
-    void createLibraryTasks() {
+    void createLibraryTasks(BuildTypeData buildTypeData) {
         ProductFlavorData defaultConfigData = getDefaultConfigData();
 
         def variantConfig = new VariantConfiguration(
                 defaultConfigData.productFlavor, defaultConfigData.androidSourceSet,
-                debugBuildTypeData.buildType, debugBuildTypeData.androidSourceSet,
+                buildTypeData.buildType, buildTypeData.androidSourceSet,
                 VariantConfiguration.Type.LIBRARY)
 
         ProductionAppVariant variant = new ProductionAppVariant(variantConfig)
@@ -77,10 +83,39 @@ class AndroidLibraryPlugin extends AndroidBasePlugin implements Plugin<Project>
 
         // Add a compile task
         createCompileTask(variant, null/*testedVariant*/, processResources, generateBuildConfigTask)
+
+        // jar the classes.
+        Jar jar = project.tasks.add("${buildTypeData.buildType.name}Jar", Jar);
+        jar.from(variant.compileTask.outputs);
+        jar.destinationDir = project.file("$project.buildDir/$DIR_BUNDLES/${variant.dirName}")
+        jar.baseName = "classes"
+
+        // merge the resources into the bundle folder
+        Copy mergeRes = project.tasks.add("merge${variant.name}Res",
+                Copy)
+        // mergeRes from 3 sources. the order is important to make sure the override works well.
+        // TODO: fix the case of values -- need to merge the XML!
+        mergeRes.from(defaultConfigData.androidSourceSet.androidResources,
+                buildTypeData.androidSourceSet.androidResources, crunchTask.outputDir)
+        mergeRes.into(project.file("$project.buildDir/$DIR_BUNDLES/${variant.dirName}/res"))
+
+        Zip bundle = project.tasks.add("bundle${variant.name}", Zip)
+        bundle.dependsOn jar, mergeRes
+        bundle.setDescription("Assembles a bundle containing the library in ${variant.name}.");
+        bundle.destinationDir = project.file("$project.buildDir/libs")
+        bundle.extension = "alb"
+        bundle.baseName = variant.baseName
+        bundle.from(project.file("$project.buildDir/$DIR_BUNDLES/${variant.dirName}"))
+
+        buildTypeData.assembleTask.dependsOn bundle
     }
 
     @Override
     String getTarget() {
         return extension.target
     }
+
+    protected String getManifestOutDir() {
+        return DIR_BUNDLES
+    }
 }
index 78830cb..a186141 100644 (file)
@@ -27,6 +27,7 @@ import org.gradle.api.Plugin
 import org.gradle.api.Project
 import org.gradle.api.Task
 import org.gradle.internal.reflect.Instantiator
+import org.gradle.api.plugins.BasePlugin
 
 class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
     private final Map<String, BuildTypeData> buildTypes = [:]
@@ -53,8 +54,6 @@ class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
                 buildTypes, productFlavors)
         setDefaultConfig(extension.defaultConfig)
 
-        findSdk(project)
-
         buildTypes.whenObjectAdded { BuildType buildType ->
             addBuildType(buildType)
         }
@@ -133,7 +132,7 @@ class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
         } else {
             // there'll be more than one test app, so we need a top level assembleTest
             assembleTest = project.tasks.add("assembleTest")
-            assembleTest.group = "Build"
+            assembleTest.group = BasePlugin.BUILD_GROUP
             assembleTest.description = "Assembles all the Test applications"
 
             productFlavors.values().each { ProductFlavorData productFlavorData ->
@@ -367,7 +366,7 @@ class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
         if (assembleTask == null) {
             assembleTask = project.tasks.add("assemble${variant.name}")
             assembleTask.description = "Assembles the " + variant.description
-            assembleTask.group = "Build"
+            assembleTask.group = BasePlugin.BUILD_GROUP
             returnTask = assembleTask
         }
         assembleTask.dependsOn appTask
@@ -375,7 +374,7 @@ class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
         // add an uninstall task
         def uninstallApp = project.tasks.add("uninstall${variant.name}", UninstallApplication)
         uninstallApp.description = "Uninstalls the " + variant.description
-        uninstallApp.group = "Install"
+        uninstallApp.group = AndroidBasePlugin.INSTALL_GROUP
         uninstallApp.variant = variant
         uninstallApp.sdkDir = sdkDir
 
@@ -388,4 +387,9 @@ class AndroidPlugin extends AndroidBasePlugin implements Plugin<Project> {
     String getTarget() {
         return extension.target;
     }
+
+    protected String getManifestOutDir() {
+        return "manifests"
+    }
+
 }
index d314ab6..758b8da 100644 (file)
@@ -17,6 +17,8 @@ package com.android.build.gradle
 
 import com.android.builder.ProductFlavor
 import org.gradle.api.Action
+import com.android.build.gradle.internal.AaptOptionsImpl
+import com.android.build.gradle.internal.DexOptionsImpl
 
 /**
  * Base android extension for all android plugins.
@@ -26,10 +28,21 @@ class BaseAndroidExtension {
     String target
     final ProductFlavor defaultConfig = new ProductFlavor("main");
 
+    final AaptOptionsImpl aaptOptions = new AaptOptionsImpl()
+    final DexOptionsImpl dexOptions = new DexOptionsImpl()
+
     BaseAndroidExtension() {
     }
 
     void defaultConfig(Action<ProductFlavor> action) {
         action.execute(defaultConfig)
     }
+
+    void aaptOptions(Action<AaptOptionsImpl> action) {
+        action.execute(aaptOptions)
+    }
+
+    void dexOptions(Action<DexOptionsImpl> action) {
+        action.execute(dexOptions)
+    }
 }
diff --git a/testapps/tictactoe/README.txt b/testapps/tictactoe/README.txt
new file mode 100644 (file)
index 0000000..6a1ac65
--- /dev/null
@@ -0,0 +1,21 @@
+Sample: tictactoe/lib and tictactoe/app\r
+\r
+--------\r
+Summary:\r
+--------\r
+\r
+These two projects work together. They demonstrate how to use the ability to\r
+split an APK into multiple projects.\r
+\r
+--------\r
+Details:\r
+--------\r
+\r
+'app' is the main project. It defines a main activity that is first\r
+displayed to the user. When one of the start buttons is selected, an\r
+activity defined in 'lib' is started.\r
+\r
+For more details on the purpose of this feature, its limitations and detailed usage,\r
+please read the SDK guide at\r
+  http://developer.android.com/guide/developing/eclipse-adt.html\r
+\r
diff --git a/testapps/tictactoe/app/build.gradle b/testapps/tictactoe/app/build.gradle
new file mode 100644 (file)
index 0000000..59f68b5
--- /dev/null
@@ -0,0 +1,8 @@
+apply plugin: 'android'
+
+//
+// A basic Android application split over a library and a main project.
+//
+dependencies {
+    compile project(':lib')
+}
diff --git a/testapps/tictactoe/app/src/main/AndroidManifest.xml b/testapps/tictactoe/app/src/main/AndroidManifest.xml
new file mode 100755 (executable)
index 0000000..e62c9ed
--- /dev/null
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.example.android.tictactoe">
+
+    <uses-sdk android:minSdkVersion="8" />
+
+    <application android:icon="@drawable/icon" android:label="@string/app_name">
+        <activity android:name=".MainActivity"
+                  android:label="@string/app_name">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/testapps/tictactoe/app/src/main/java/com/example/android/tictactoe/MainActivity.java b/testapps/tictactoe/app/src/main/java/com/example/android/tictactoe/MainActivity.java
new file mode 100755 (executable)
index 0000000..14a9011
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 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.example.android.tictactoe;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.example.android.tictactoe.library.GameActivity;
+import com.example.android.tictactoe.library.GameView.State;
+
+public class MainActivity extends Activity {
+    /** Called when the activity is first created. */
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.main);
+
+        findViewById(R.id.start_player).setOnClickListener(
+                new OnClickListener() {
+            public void onClick(View v) {
+                startGame(true);
+            }
+        });
+
+        findViewById(R.id.start_comp).setOnClickListener(
+                new OnClickListener() {
+            public void onClick(View v) {
+                startGame(false);
+            }
+        });
+    }
+
+    private void startGame(boolean startWithHuman) {
+        Intent i = new Intent(this, GameActivity.class);
+        i.putExtra(GameActivity.EXTRA_START_PLAYER,
+                startWithHuman ? State.PLAYER1.getValue() : State.PLAYER2.getValue());
+        startActivity(i);
+    }
+}
\ No newline at end of file
diff --git a/testapps/tictactoe/app/src/main/res/drawable/icon.png b/testapps/tictactoe/app/src/main/res/drawable/icon.png
new file mode 100755 (executable)
index 0000000..b8665ff
Binary files /dev/null and b/testapps/tictactoe/app/src/main/res/drawable/icon.png differ
diff --git a/testapps/tictactoe/app/src/main/res/layout/main.xml b/testapps/tictactoe/app/src/main/res/layout/main.xml
new file mode 100755 (executable)
index 0000000..1e75004
--- /dev/null
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:gravity="center_horizontal"
+    >
+
+    <TextView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginTop="20dip"
+        android:layout_marginBottom="5dip"
+        android:text="@string/welcome"
+        />
+    <TextView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginBottom="5dip"
+        android:text="@string/explain2"
+        />
+    <TextView
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginBottom="20dip"
+        android:text="@string/explain1"
+        />
+
+    <Button
+        android:id="@+id/start_player"
+        android:text="@string/start_player"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        />
+    <Button
+        android:id="@+id/start_comp"
+        android:text="@string/start_comp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginBottom="10dip"
+        />
+
+    <ImageView
+        android:id="@+id/ImageView01"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:src="@drawable/icon"
+        />
+
+</LinearLayout>
diff --git a/testapps/tictactoe/app/src/main/res/values/strings.xml b/testapps/tictactoe/app/src/main/res/values/strings.xml
new file mode 100755 (executable)
index 0000000..436877f
--- /dev/null
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<resources>
+    <string name="start_comp">Start -- Computer goes first</string>
+    <string name="start_player">Start -- Player goes first</string>
+    <string name="welcome"><b>Welcome to the Tic-Tac-Toe Sample!</b></string>\r
+    <string name="explain1">This sample code demonstrates how to split an application in multiple projects by using the \'project library\' available in the Froyo SDK Tools.</string>
+    <string name="explain2">This activity is defined in one project. The second activity, launched by one of the buttons below, is located in another project which is a \"library\" to the main one and merged in the same APK.</string>
+    <string name="app_name">Tic-Tac-Toe Sample</string>
+</resources>
diff --git a/testapps/tictactoe/build.gradle b/testapps/tictactoe/build.gradle
new file mode 100644 (file)
index 0000000..0c73a9f
--- /dev/null
@@ -0,0 +1,8 @@
+buildscript {
+    repositories {
+        maven { url '../../repo' }
+    }
+    dependencies {
+        classpath 'com.android.build:gradle-android:0.1-SNAPSHOT'
+    }
+}
diff --git a/testapps/tictactoe/lib/build.gradle b/testapps/tictactoe/lib/build.gradle
new file mode 100644 (file)
index 0000000..f7838fb
--- /dev/null
@@ -0,0 +1,5 @@
+apply plugin: 'android-library'
+
+android {
+    target = "android-15"
+}
\ No newline at end of file
diff --git a/testapps/tictactoe/lib/src/main/AndroidManifest.xml b/testapps/tictactoe/lib/src/main/AndroidManifest.xml
new file mode 100755 (executable)
index 0000000..ee934a5
--- /dev/null
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.example.android.tictactoe.library">
+    <application>
+        <activity android:name="GameActivity" />
+    </application>
+</manifest>
diff --git a/testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameActivity.java b/testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameActivity.java
new file mode 100755 (executable)
index 0000000..df1cac0
--- /dev/null
@@ -0,0 +1,259 @@
+/*\r
+ * Copyright (C) 2010 The Android Open Source Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.example.android.tictactoe.library;\r
+\r
+import java.util.Random;\r
+\r
+import android.app.Activity;\r
+import android.os.Bundle;\r
+import android.os.Handler;\r
+import android.os.Message;\r
+import android.os.Handler.Callback;\r
+import android.view.View;\r
+import android.view.View.OnClickListener;\r
+import android.widget.Button;\r
+import android.widget.TextView;\r
+\r
+import com.example.android.tictactoe.library.GameView.ICellListener;\r
+import com.example.android.tictactoe.library.GameView.State;\r
+\r
+\r
+public class GameActivity extends Activity {\r
+\r
+    /** Start player. Must be 1 or 2. Default is 1. */\r
+    public static final String EXTRA_START_PLAYER =\r
+        "com.example.android.tictactoe.library.GameActivity.EXTRA_START_PLAYER";\r
+\r
+    private static final int MSG_COMPUTER_TURN = 1;\r
+    private static final long COMPUTER_DELAY_MS = 500;\r
+\r
+    private Handler mHandler = new Handler(new MyHandlerCallback());\r
+    private Random mRnd = new Random();\r
+    private GameView mGameView;\r
+    private TextView mInfoView;\r
+    private Button mButtonNext;\r
+\r
+    /** Called when the activity is first created. */\r
+    @Override\r
+    public void onCreate(Bundle bundle) {\r
+        super.onCreate(bundle);\r
+\r
+        /*\r
+         * IMPORTANT: all resource IDs from this library will eventually be merged\r
+         * with the resources from the main project that will use the library.\r
+         *\r
+         * If the main project and the libraries define the same resource IDs,\r
+         * the application project will always have priority and override library resources\r
+         * and IDs defined in multiple libraries are resolved based on the libraries priority\r
+         * defined in the main project.\r
+         *\r
+         * An intentional consequence is that the main project can override some resources\r
+         * from the library.\r
+         * (TODO insert example).\r
+         *\r
+         * To avoid potential conflicts, it is suggested to add a prefix to the\r
+         * library resource names.\r
+         */\r
+        setContentView(R.layout.lib_game);\r
+\r
+        mGameView = (GameView) findViewById(R.id.game_view);\r
+        mInfoView = (TextView) findViewById(R.id.info_turn);\r
+        mButtonNext = (Button) findViewById(R.id.next_turn);\r
+\r
+        mGameView.setFocusable(true);\r
+        mGameView.setFocusableInTouchMode(true);\r
+        mGameView.setCellListener(new MyCellListener());\r
+\r
+        mButtonNext.setOnClickListener(new MyButtonListener());\r
+    }\r
+\r
+    @Override\r
+    protected void onResume() {\r
+        super.onResume();\r
+\r
+        State player = mGameView.getCurrentPlayer();\r
+        if (player == State.UNKNOWN) {\r
+            player = State.fromInt(getIntent().getIntExtra(EXTRA_START_PLAYER, 1));\r
+            if (!checkGameFinished(player)) {\r
+                selectTurn(player);\r
+            }\r
+        }\r
+        if (player == State.PLAYER2) {\r
+            mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS);\r
+        }\r
+        if (player == State.WIN) {\r
+            setWinState(mGameView.getWinner());\r
+        }\r
+    }\r
+\r
+\r
+    private State selectTurn(State player) {\r
+        mGameView.setCurrentPlayer(player);\r
+        mButtonNext.setEnabled(false);\r
+\r
+        if (player == State.PLAYER1) {\r
+            mInfoView.setText(R.string.player1_turn);\r
+            mGameView.setEnabled(true);\r
+\r
+        } else if (player == State.PLAYER2) {\r
+            mInfoView.setText(R.string.player2_turn);\r
+            mGameView.setEnabled(false);\r
+        }\r
+\r
+        return player;\r
+    }\r
+\r
+    private class MyCellListener implements ICellListener {\r
+        public void onCellSelected() {\r
+            if (mGameView.getCurrentPlayer() == State.PLAYER1) {\r
+                int cell = mGameView.getSelection();\r
+                mButtonNext.setEnabled(cell >= 0);\r
+            }\r
+        }\r
+    }\r
+\r
+    private class MyButtonListener implements OnClickListener {\r
+\r
+        public void onClick(View v) {\r
+            State player = mGameView.getCurrentPlayer();\r
+\r
+            if (player == State.WIN) {\r
+                GameActivity.this.finish();\r
+\r
+            } else if (player == State.PLAYER1) {\r
+                int cell = mGameView.getSelection();\r
+                if (cell >= 0) {\r
+                    mGameView.stopBlink();\r
+                    mGameView.setCell(cell, player);\r
+                    finishTurn();\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    private class MyHandlerCallback implements Callback {\r
+        public boolean handleMessage(Message msg) {\r
+            if (msg.what == MSG_COMPUTER_TURN) {\r
+\r
+                // Pick a non-used cell at random. That's about all the AI you need for this game.\r
+                State[] data = mGameView.getData();\r
+                int used = 0;\r
+                while (used != 0x1F) {\r
+                    int index = mRnd.nextInt(9);\r
+                    if (((used >> index) & 1) == 0) {\r
+                        used |= 1 << index;\r
+                        if (data[index] == State.EMPTY) {\r
+                            mGameView.setCell(index, mGameView.getCurrentPlayer());\r
+                            break;\r
+                        }\r
+                    }\r
+                }\r
+\r
+                finishTurn();\r
+                return true;\r
+            }\r
+            return false;\r
+        }\r
+    }\r
+\r
+    private State getOtherPlayer(State player) {\r
+        return player == State.PLAYER1 ? State.PLAYER2 : State.PLAYER1;\r
+    }\r
+\r
+    private void finishTurn() {\r
+        State player = mGameView.getCurrentPlayer();\r
+        if (!checkGameFinished(player)) {\r
+            player = selectTurn(getOtherPlayer(player));\r
+            if (player == State.PLAYER2) {\r
+                mHandler.sendEmptyMessageDelayed(MSG_COMPUTER_TURN, COMPUTER_DELAY_MS);\r
+            }\r
+        }\r
+    }\r
+\r
+    public boolean checkGameFinished(State player) {\r
+        State[] data = mGameView.getData();\r
+        boolean full = true;\r
+\r
+        int col = -1;\r
+        int row = -1;\r
+        int diag = -1;\r
+\r
+        // check rows\r
+        for (int j = 0, k = 0; j < 3; j++, k += 3) {\r
+            if (data[k] != State.EMPTY && data[k] == data[k+1] && data[k] == data[k+2]) {\r
+                row = j;\r
+            }\r
+            if (full && (data[k] == State.EMPTY ||\r
+                         data[k+1] == State.EMPTY ||\r
+                         data[k+2] == State.EMPTY)) {\r
+                full = false;\r
+            }\r
+        }\r
+\r
+        // check columns\r
+        for (int i = 0; i < 3; i++) {\r
+            if (data[i] != State.EMPTY && data[i] == data[i+3] && data[i] == data[i+6]) {\r
+                col = i;\r
+            }\r
+        }\r
+\r
+        // check diagonals\r
+        if (data[0] != State.EMPTY && data[0] == data[1+3] && data[0] == data[2+6]) {\r
+            diag = 0;\r
+        } else  if (data[2] != State.EMPTY && data[2] == data[1+3] && data[2] == data[0+6]) {\r
+            diag = 1;\r
+        }\r
+\r
+        if (col != -1 || row != -1 || diag != -1) {\r
+            setFinished(player, col, row, diag);\r
+            return true;\r
+        }\r
+\r
+        // if we get here, there's no winner but the board is full.\r
+        if (full) {\r
+            setFinished(State.EMPTY, -1, -1, -1);\r
+            return true;\r
+        }\r
+        return false;\r
+    }\r
+\r
+    private void setFinished(State player, int col, int row, int diagonal) {\r
+\r
+        mGameView.setCurrentPlayer(State.WIN);\r
+        mGameView.setWinner(player);\r
+        mGameView.setEnabled(false);\r
+        mGameView.setFinished(col, row, diagonal);\r
+\r
+        setWinState(player);\r
+    }\r
+\r
+    private void setWinState(State player) {\r
+        mButtonNext.setEnabled(true);\r
+        mButtonNext.setText("Back");\r
+\r
+        String text;\r
+\r
+        if (player == State.EMPTY) {\r
+            text = getString(R.string.tie);\r
+        } else if (player == State.PLAYER1) {\r
+            text = getString(R.string.player1_win);\r
+        } else {\r
+            text = getString(R.string.player2_win);\r
+        }\r
+        mInfoView.setText(text);\r
+    }\r
+}\r
diff --git a/testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameView.java b/testapps/tictactoe/lib/src/main/java/com/example/android/tictactoe/library/GameView.java
new file mode 100755 (executable)
index 0000000..3af516a
--- /dev/null
@@ -0,0 +1,468 @@
+/*\r
+ * Copyright (C) 2010 The Android Open Source Project\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *      http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+\r
+package com.example.android.tictactoe.library;\r
+\r
+import java.util.Random;\r
+\r
+import android.content.Context;\r
+import android.content.res.Resources;\r
+import android.graphics.Bitmap;\r
+import android.graphics.BitmapFactory;\r
+import android.graphics.Canvas;\r
+import android.graphics.Paint;\r
+import android.graphics.Rect;\r
+import android.graphics.Bitmap.Config;\r
+import android.graphics.BitmapFactory.Options;\r
+import android.graphics.Paint.Style;\r
+import android.graphics.drawable.Drawable;\r
+import android.os.Bundle;\r
+import android.os.Handler;\r
+import android.os.Message;\r
+import android.os.Parcelable;\r
+import android.os.Handler.Callback;\r
+import android.util.AttributeSet;\r
+import android.view.MotionEvent;\r
+import android.view.View;\r
+\r
+//-----------------------------------------------\r
+\r
+public class GameView extends View {\r
+\r
+    public static final long FPS_MS = 1000/2;\r
+\r
+    public enum State {\r
+        UNKNOWN(-3),\r
+        WIN(-2),\r
+        EMPTY(0),\r
+        PLAYER1(1),\r
+        PLAYER2(2);\r
+\r
+        private int mValue;\r
+\r
+        private State(int value) {\r
+            mValue = value;\r
+        }\r
+\r
+        public int getValue() {\r
+            return mValue;\r
+        }\r
+\r
+        public static State fromInt(int i) {\r
+            for (State s : values()) {\r
+                if (s.getValue() == i) {\r
+                    return s;\r
+                }\r
+            }\r
+            return EMPTY;\r
+        }\r
+    }\r
+\r
+    private static final int MARGIN = 4;\r
+    private static final int MSG_BLINK = 1;\r
+\r
+    private final Handler mHandler = new Handler(new MyHandler());\r
+\r
+    private final Rect mSrcRect = new Rect();\r
+    private final Rect mDstRect = new Rect();\r
+\r
+    private int mSxy;\r
+    private int mOffetX;\r
+    private int mOffetY;\r
+    private Paint mWinPaint;\r
+    private Paint mLinePaint;\r
+    private Paint mBmpPaint;\r
+    private Bitmap mBmpPlayer1;\r
+    private Bitmap mBmpPlayer2;\r
+    private Drawable mDrawableBg;\r
+\r
+    private ICellListener mCellListener;\r
+\r
+    /** Contains one of {@link State#EMPTY}, {@link State#PLAYER1} or {@link State#PLAYER2}. */\r
+    private final State[] mData = new State[9];\r
+\r
+    private int mSelectedCell = -1;\r
+    private State mSelectedValue = State.EMPTY;\r
+    private State mCurrentPlayer = State.UNKNOWN;\r
+    private State mWinner = State.EMPTY;\r
+\r
+    private int mWinCol = -1;\r
+    private int mWinRow = -1;\r
+    private int mWinDiag = -1;\r
+\r
+    private boolean mBlinkDisplayOff;\r
+    private final Rect mBlinkRect = new Rect();\r
+\r
+\r
+\r
+    public interface ICellListener {\r
+        abstract void onCellSelected();\r
+    }\r
+\r
+    public GameView(Context context, AttributeSet attrs) {\r
+        super(context, attrs);\r
+        requestFocus();\r
+\r
+        mDrawableBg = getResources().getDrawable(R.drawable.lib_bg);\r
+        setBackgroundDrawable(mDrawableBg);\r
+\r
+        mBmpPlayer1 = getResBitmap(R.drawable.lib_cross);\r
+        mBmpPlayer2 = getResBitmap(R.drawable.lib_circle);\r
+\r
+        if (mBmpPlayer1 != null) {\r
+            mSrcRect.set(0, 0, mBmpPlayer1.getWidth() -1, mBmpPlayer1.getHeight() - 1);\r
+        }\r
+\r
+        mBmpPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\r
+\r
+        mLinePaint = new Paint();\r
+        mLinePaint.setColor(0xFFFFFFFF);\r
+        mLinePaint.setStrokeWidth(5);\r
+        mLinePaint.setStyle(Style.STROKE);\r
+\r
+        mWinPaint = new Paint(Paint.ANTI_ALIAS_FLAG);\r
+        mWinPaint.setColor(0xFFFF0000);\r
+        mWinPaint.setStrokeWidth(10);\r
+        mWinPaint.setStyle(Style.STROKE);\r
+\r
+        for (int i = 0; i < mData.length; i++) {\r
+            mData[i] = State.EMPTY;\r
+        }\r
+\r
+        if (isInEditMode()) {\r
+            // In edit mode (e.g. in the Eclipse ADT graphical layout editor)\r
+            // we'll use some random data to display the state.\r
+            Random rnd = new Random();\r
+            for (int i = 0; i < mData.length; i++) {\r
+                mData[i] = State.fromInt(rnd.nextInt(3));\r
+            }\r
+        }\r
+    }\r
+\r
+    public State[] getData() {\r
+        return mData;\r
+    }\r
+\r
+    public void setCell(int cellIndex, State value) {\r
+        mData[cellIndex] = value;\r
+        invalidate();\r
+    }\r
+\r
+    public void setCellListener(ICellListener cellListener) {\r
+        mCellListener = cellListener;\r
+    }\r
+\r
+    public int getSelection() {\r
+        if (mSelectedValue == mCurrentPlayer) {\r
+            return mSelectedCell;\r
+        }\r
+\r
+        return -1;\r
+    }\r
+\r
+    public State getCurrentPlayer() {\r
+        return mCurrentPlayer;\r
+    }\r
+\r
+    public void setCurrentPlayer(State player) {\r
+        mCurrentPlayer = player;\r
+        mSelectedCell = -1;\r
+    }\r
+\r
+    public State getWinner() {\r
+        return mWinner;\r
+    }\r
+\r
+    public void setWinner(State winner) {\r
+        mWinner = winner;\r
+    }\r
+\r
+    /** Sets winning mark on specified column or row (0..2) or diagonal (0..1). */\r
+    public void setFinished(int col, int row, int diagonal) {\r
+        mWinCol = col;\r
+        mWinRow = row;\r
+        mWinDiag = diagonal;\r
+    }\r
+\r
+    //-----------------------------------------\r
+\r
+\r
+    @Override\r
+    protected void onDraw(Canvas canvas) {\r
+        super.onDraw(canvas);\r
+\r
+        int sxy = mSxy;\r
+        int s3  = sxy * 3;\r
+        int x7 = mOffetX;\r
+        int y7 = mOffetY;\r
+\r
+        for (int i = 0, k = sxy; i < 2; i++, k += sxy) {\r
+            canvas.drawLine(x7    , y7 + k, x7 + s3 - 1, y7 + k     , mLinePaint);\r
+            canvas.drawLine(x7 + k, y7    , x7 + k     , y7 + s3 - 1, mLinePaint);\r
+        }\r
+\r
+        for (int j = 0, k = 0, y = y7; j < 3; j++, y += sxy) {\r
+            for (int i = 0, x = x7; i < 3; i++, k++, x += sxy) {\r
+                mDstRect.offsetTo(MARGIN+x, MARGIN+y);\r
+\r
+                State v;\r
+                if (mSelectedCell == k) {\r
+                    if (mBlinkDisplayOff) {\r
+                        continue;\r
+                    }\r
+                    v = mSelectedValue;\r
+                } else {\r
+                    v = mData[k];\r
+                }\r
+\r
+                switch(v) {\r
+                case PLAYER1:\r
+                    if (mBmpPlayer1 != null) {\r
+                        canvas.drawBitmap(mBmpPlayer1, mSrcRect, mDstRect, mBmpPaint);\r
+                    }\r
+                    break;\r
+                case PLAYER2:\r
+                    if (mBmpPlayer2 != null) {\r
+                        canvas.drawBitmap(mBmpPlayer2, mSrcRect, mDstRect, mBmpPaint);\r
+                    }\r
+                    break;\r
+                }\r
+            }\r
+        }\r
+\r
+        if (mWinRow >= 0) {\r
+            int y = y7 + mWinRow * sxy + sxy / 2;\r
+            canvas.drawLine(x7 + MARGIN, y, x7 + s3 - 1 - MARGIN, y, mWinPaint);\r
+\r
+        } else if (mWinCol >= 0) {\r
+            int x = x7 + mWinCol * sxy + sxy / 2;\r
+            canvas.drawLine(x, y7 + MARGIN, x, y7 + s3 - 1 - MARGIN, mWinPaint);\r
+\r
+        } else if (mWinDiag == 0) {\r
+            // diagonal 0 is from (0,0) to (2,2)\r
+\r
+            canvas.drawLine(x7 + MARGIN, y7 + MARGIN,\r
+                    x7 + s3 - 1 - MARGIN, y7 + s3 - 1 - MARGIN, mWinPaint);\r
+\r
+        } else if (mWinDiag == 1) {\r
+            // diagonal 1 is from (0,2) to (2,0)\r
+\r
+            canvas.drawLine(x7 + MARGIN, y7 + s3 - 1 - MARGIN,\r
+                    x7 + s3 - 1 - MARGIN, y7 + MARGIN, mWinPaint);\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\r
+        // Keep the view squared\r
+        int w = MeasureSpec.getSize(widthMeasureSpec);\r
+        int h = MeasureSpec.getSize(heightMeasureSpec);\r
+        int d = w == 0 ? h : h == 0 ? w : w < h ? w : h;\r
+        setMeasuredDimension(d, d);\r
+    }\r
+\r
+    @Override\r
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {\r
+        super.onSizeChanged(w, h, oldw, oldh);\r
+\r
+        int sx = (w - 2 * MARGIN) / 3;\r
+        int sy = (h - 2 * MARGIN) / 3;\r
+\r
+        int size = sx < sy ? sx : sy;\r
+\r
+        mSxy = size;\r
+        mOffetX = (w - 3 * size) / 2;\r
+        mOffetY = (h - 3 * size) / 2;\r
+\r
+        mDstRect.set(MARGIN, MARGIN, size - MARGIN, size - MARGIN);\r
+    }\r
+\r
+    @Override\r
+    public boolean onTouchEvent(MotionEvent event) {\r
+        int action = event.getAction();\r
+\r
+        if (action == MotionEvent.ACTION_DOWN) {\r
+            return true;\r
+\r
+        } else if (action == MotionEvent.ACTION_UP) {\r
+            int x = (int) event.getX();\r
+            int y = (int) event.getY();\r
+\r
+            int sxy = mSxy;\r
+            x = (x - MARGIN) / sxy;\r
+            y = (y - MARGIN) / sxy;\r
+\r
+            if (isEnabled() && x >= 0 && x < 3 && y >= 0 & y < 3) {\r
+                int cell = x + 3 * y;\r
+\r
+                State state = cell == mSelectedCell ? mSelectedValue : mData[cell];\r
+                state = state == State.EMPTY ? mCurrentPlayer : State.EMPTY;\r
+\r
+                stopBlink();\r
+\r
+                mSelectedCell = cell;\r
+                mSelectedValue = state;\r
+                mBlinkDisplayOff = false;\r
+                mBlinkRect.set(MARGIN + x * sxy, MARGIN + y * sxy,\r
+                               MARGIN + (x + 1) * sxy, MARGIN + (y + 1) * sxy);\r
+\r
+                if (state != State.EMPTY) {\r
+                    // Start the blinker\r
+                    mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS);\r
+                }\r
+\r
+                if (mCellListener != null) {\r
+                    mCellListener.onCellSelected();\r
+                }\r
+            }\r
+\r
+            return true;\r
+        }\r
+\r
+        return false;\r
+    }\r
+\r
+    public void stopBlink() {\r
+        boolean hadSelection = mSelectedCell != -1 && mSelectedValue != State.EMPTY;\r
+        mSelectedCell = -1;\r
+        mSelectedValue = State.EMPTY;\r
+        if (!mBlinkRect.isEmpty()) {\r
+            invalidate(mBlinkRect);\r
+        }\r
+        mBlinkDisplayOff = false;\r
+        mBlinkRect.setEmpty();\r
+        mHandler.removeMessages(MSG_BLINK);\r
+        if (hadSelection && mCellListener != null) {\r
+            mCellListener.onCellSelected();\r
+        }\r
+    }\r
+\r
+    @Override\r
+    protected Parcelable onSaveInstanceState() {\r
+        Bundle b = new Bundle();\r
+\r
+        Parcelable s = super.onSaveInstanceState();\r
+        b.putParcelable("gv_super_state", s);\r
+\r
+        b.putBoolean("gv_en", isEnabled());\r
+\r
+        int[] data = new int[mData.length];\r
+        for (int i = 0; i < data.length; i++) {\r
+            data[i] = mData[i].getValue();\r
+        }\r
+        b.putIntArray("gv_data", data);\r
+\r
+        b.putInt("gv_sel_cell", mSelectedCell);\r
+        b.putInt("gv_sel_val",  mSelectedValue.getValue());\r
+        b.putInt("gv_curr_play", mCurrentPlayer.getValue());\r
+        b.putInt("gv_winner", mWinner.getValue());\r
+\r
+        b.putInt("gv_win_col", mWinCol);\r
+        b.putInt("gv_win_row", mWinRow);\r
+        b.putInt("gv_win_diag", mWinDiag);\r
+\r
+        b.putBoolean("gv_blink_off", mBlinkDisplayOff);\r
+        b.putParcelable("gv_blink_rect", mBlinkRect);\r
+\r
+        return b;\r
+    }\r
+\r
+    @Override\r
+    protected void onRestoreInstanceState(Parcelable state) {\r
+\r
+        if (!(state instanceof Bundle)) {\r
+            // Not supposed to happen.\r
+            super.onRestoreInstanceState(state);\r
+            return;\r
+        }\r
+\r
+        Bundle b = (Bundle) state;\r
+        Parcelable superState = b.getParcelable("gv_super_state");\r
+\r
+        setEnabled(b.getBoolean("gv_en", true));\r
+\r
+        int[] data = b.getIntArray("gv_data");\r
+        if (data != null && data.length == mData.length) {\r
+            for (int i = 0; i < data.length; i++) {\r
+                mData[i] = State.fromInt(data[i]);\r
+            }\r
+        }\r
+\r
+        mSelectedCell = b.getInt("gv_sel_cell", -1);\r
+        mSelectedValue = State.fromInt(b.getInt("gv_sel_val", State.EMPTY.getValue()));\r
+        mCurrentPlayer = State.fromInt(b.getInt("gv_curr_play", State.EMPTY.getValue()));\r
+        mWinner = State.fromInt(b.getInt("gv_winner", State.EMPTY.getValue()));\r
+\r
+        mWinCol = b.getInt("gv_win_col", -1);\r
+        mWinRow = b.getInt("gv_win_row", -1);\r
+        mWinDiag = b.getInt("gv_win_diag", -1);\r
+\r
+        mBlinkDisplayOff = b.getBoolean("gv_blink_off", false);\r
+        Rect r = b.getParcelable("gv_blink_rect");\r
+        if (r != null) {\r
+            mBlinkRect.set(r);\r
+        }\r
+\r
+        // let the blink handler decide if it should blink or not\r
+        mHandler.sendEmptyMessage(MSG_BLINK);\r
+\r
+        super.onRestoreInstanceState(superState);\r
+    }\r
+\r
+    //-----\r
+\r
+    private class MyHandler implements Callback {\r
+        public boolean handleMessage(Message msg) {\r
+            if (msg.what == MSG_BLINK) {\r
+                if (mSelectedCell >= 0 && mSelectedValue != State.EMPTY && mBlinkRect.top != 0) {\r
+                    mBlinkDisplayOff = !mBlinkDisplayOff;\r
+                    invalidate(mBlinkRect);\r
+\r
+                    if (!mHandler.hasMessages(MSG_BLINK)) {\r
+                        mHandler.sendEmptyMessageDelayed(MSG_BLINK, FPS_MS);\r
+                    }\r
+                }\r
+                return true;\r
+            }\r
+            return false;\r
+        }\r
+    }\r
+\r
+    private Bitmap getResBitmap(int bmpResId) {\r
+        Options opts = new Options();\r
+        opts.inDither = false;\r
+\r
+        Resources res = getResources();\r
+        Bitmap bmp = BitmapFactory.decodeResource(res, bmpResId, opts);\r
+\r
+        if (bmp == null && isInEditMode()) {\r
+            // BitmapFactory.decodeResource doesn't work from the rendering\r
+            // library in Eclipse's Graphical Layout Editor. Use this workaround instead.\r
+\r
+            Drawable d = res.getDrawable(bmpResId);\r
+            int w = d.getIntrinsicWidth();\r
+            int h = d.getIntrinsicHeight();\r
+            bmp = Bitmap.createBitmap(w, h, Config.ARGB_8888);\r
+            Canvas c = new Canvas(bmp);\r
+            d.setBounds(0, 0, w - 1, h - 1);\r
+            d.draw(c);\r
+        }\r
+\r
+        return bmp;\r
+    }\r
+}\r
+\r
+\r
diff --git a/testapps/tictactoe/lib/src/main/res/drawable/lib_bg.9.png b/testapps/tictactoe/lib/src/main/res/drawable/lib_bg.9.png
new file mode 100755 (executable)
index 0000000..38c06c0
Binary files /dev/null and b/testapps/tictactoe/lib/src/main/res/drawable/lib_bg.9.png differ
diff --git a/testapps/tictactoe/lib/src/main/res/drawable/lib_circle.png b/testapps/tictactoe/lib/src/main/res/drawable/lib_circle.png
new file mode 100755 (executable)
index 0000000..55adffe
Binary files /dev/null and b/testapps/tictactoe/lib/src/main/res/drawable/lib_circle.png differ
diff --git a/testapps/tictactoe/lib/src/main/res/drawable/lib_cross.png b/testapps/tictactoe/lib/src/main/res/drawable/lib_cross.png
new file mode 100755 (executable)
index 0000000..9189ebb
Binary files /dev/null and b/testapps/tictactoe/lib/src/main/res/drawable/lib_cross.png differ
diff --git a/testapps/tictactoe/lib/src/main/res/layout-land/lib_game.xml b/testapps/tictactoe/lib/src/main/res/layout-land/lib_game.xml
new file mode 100755 (executable)
index 0000000..9777e02
--- /dev/null
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:gravity="center_vertical|center_horizontal"
+    >
+
+    <com.example.android.tictactoe.library.GameView
+        android:id="@+id/game_view"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+        android:layout_margin="20dip"
+        />
+
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        >
+
+        <TextView
+            android:id="@+id/info_turn"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:layout_marginBottom="10dip"
+            />
+
+        <Button
+            android:id="@+id/next_turn"
+            android:text="I'm done"
+            android:minEms="10"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="20dip"
+            android:layout_marginRight="20dip"
+            />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/testapps/tictactoe/lib/src/main/res/layout/lib_game.xml b/testapps/tictactoe/lib/src/main/res/layout/lib_game.xml
new file mode 100755 (executable)
index 0000000..6735983
--- /dev/null
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:gravity="center_horizontal"
+    >
+
+    <com.example.android.tictactoe.library.GameView
+        android:id="@+id/game_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_margin="20dip"
+        android:layout_weight="1"
+        />
+
+    <TextView
+        android:id="@+id/info_turn"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center_horizontal"
+        android:layout_marginBottom="10dip"
+        />
+
+    <Button
+        android:id="@+id/next_turn"
+        android:text="I'm done"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="20dip"
+        android:layout_marginRight="20dip"
+        />
+
+</LinearLayout>
diff --git a/testapps/tictactoe/lib/src/main/res/values/strings.xml b/testapps/tictactoe/lib/src/main/res/values/strings.xml
new file mode 100755 (executable)
index 0000000..468975a
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2010 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.
+-->
+
+<resources>
+    <string name="player2_win">Player 2 (computer) wins!</string>
+    <string name="player1_win">Player 1 (you) wins!</string>
+    <string name="tie">This is a tie! No one wins!</string>
+    <string name="player2_turn">Player 2\'s turn (that\'s the computer)</string>
+    <string name="player1_turn">Player 1\'s turn -- that\'s you!</string>
+</resources>
diff --git a/testapps/tictactoe/settings.gradle b/testapps/tictactoe/settings.gradle
new file mode 100644 (file)
index 0000000..5ed7972
--- /dev/null
@@ -0,0 +1,2 @@
+include 'app'
+include 'lib'
\ No newline at end of file