aboutsummaryrefslogtreecommitdiff
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/.gitignore4
-rw-r--r--apps/NotificationStudio/.classpath8
-rw-r--r--apps/NotificationStudio/.project33
-rw-r--r--apps/NotificationStudio/.settings/org.eclipse.jdt.core.prefs4
-rw-r--r--apps/NotificationStudio/AndroidManifest.xml38
-rw-r--r--apps/NotificationStudio/libs/android-support-v4.jarbin0 -> 351326 bytes
-rw-r--r--apps/NotificationStudio/project.properties14
-rw-r--r--apps/NotificationStudio/res/drawable-hdpi/android_logo.gifbin0 -> 4678 bytes
-rw-r--r--apps/NotificationStudio/res/drawable-hdpi/ic_notification_multiple_mail_holo_dark.pngbin0 -> 378 bytes
-rw-r--r--apps/NotificationStudio/res/drawable-hdpi/romain.jpgbin0 -> 8832 bytes
-rw-r--r--apps/NotificationStudio/res/drawable-nodpi/icon_bg.xml29
-rw-r--r--apps/NotificationStudio/res/drawable-nodpi/romainguy_rockaway.jpgbin0 -> 414841 bytes
-rw-r--r--apps/NotificationStudio/res/layout-v14/boolean_editor.xml21
-rw-r--r--apps/NotificationStudio/res/layout-v16/studio.xml33
-rw-r--r--apps/NotificationStudio/res/layout-w801dp/studio.xml28
-rw-r--r--apps/NotificationStudio/res/layout/boolean_editor.xml20
-rw-r--r--apps/NotificationStudio/res/layout/divider.xml40
-rw-r--r--apps/NotificationStudio/res/layout/editable_item.xml111
-rw-r--r--apps/NotificationStudio/res/layout/editors.xml29
-rw-r--r--apps/NotificationStudio/res/layout/preview.xml56
-rw-r--r--apps/NotificationStudio/res/layout/studio.xml26
-rw-r--r--apps/NotificationStudio/res/menu/action_bar.xml30
-rw-r--r--apps/NotificationStudio/res/values-sw600dp/dimens.xml24
-rw-r--r--apps/NotificationStudio/res/values-sw720dp/dimens.xml21
-rw-r--r--apps/NotificationStudio/res/values-v11/colors.xml21
-rw-r--r--apps/NotificationStudio/res/values-v11/dimens.xml21
-rw-r--r--apps/NotificationStudio/res/values/colors.xml23
-rw-r--r--apps/NotificationStudio/res/values/dimens.xml33
-rw-r--r--apps/NotificationStudio/res/values/strings.xml55
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/MaxHeightScrollView.java41
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/NotificationStudioActivity.java249
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/action/ShareCodeAction.java35
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/action/ShareMockupAction.java79
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/BitmapEditor.java36
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/BooleanEditor.java49
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/DateTimeEditor.java139
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/DropDownEditor.java84
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/Editors.java100
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/IconEditor.java118
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/IntEditor.java32
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/LinesEditor.java28
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/editor/TextEditor.java73
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/generator/CodeGenerator.java139
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/generator/NotificationGenerator.java131
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItem.java216
-rw-r--r--apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItemConstants.java74
-rwxr-xr-xapps/SdkController/.classpath8
-rwxr-xr-xapps/SdkController/.project34
-rwxr-xr-xapps/SdkController/.settings/org.eclipse.jdt.core.prefs12
-rwxr-xr-xapps/SdkController/AndroidManifest.xml44
-rwxr-xr-xapps/SdkController/Implementation.txt85
-rw-r--r--apps/SdkController/NOTICE190
-rwxr-xr-xapps/SdkController/assets/intro_help.html42
-rwxr-xr-xapps/SdkController/bin/SdkControllerApp.apkbin0 -> 73873 bytes
-rwxr-xr-xapps/SdkController/proguard-project.txt20
-rwxr-xr-xapps/SdkController/project.properties14
-rwxr-xr-xapps/SdkController/res/drawable-hdpi/ic_launcher.pngbin0 -> 9397 bytes
-rwxr-xr-xapps/SdkController/res/drawable-ldpi/ic_launcher.pngbin0 -> 2729 bytes
-rwxr-xr-xapps/SdkController/res/drawable-mdpi/ic_launcher.pngbin0 -> 5237 bytes
-rwxr-xr-xapps/SdkController/res/drawable-xhdpi/ic_launcher.pngbin0 -> 14383 bytes
-rwxr-xr-xapps/SdkController/res/layout-land/sensors.xml168
-rwxr-xr-xapps/SdkController/res/layout/main.xml116
-rwxr-xr-xapps/SdkController/res/layout/multitouch.xml32
-rwxr-xr-xapps/SdkController/res/layout/sensor_row.xml42
-rwxr-xr-xapps/SdkController/res/layout/sensors.xml130
-rwxr-xr-xapps/SdkController/res/values-v11/styles_v11.xml26
-rwxr-xr-xapps/SdkController/res/values/strings.xml48
-rwxr-xr-xapps/SdkController/res/values/styles.xml26
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java159
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java208
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java388
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java338
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchChannel.java173
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorChannel.java675
-rw-r--r--apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java795
-rw-r--r--apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java412
-rw-r--r--apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java146
-rw-r--r--apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java213
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java319
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java57
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java36
-rwxr-xr-xapps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java232
82 files changed, 7533 insertions, 0 deletions
diff --git a/apps/.gitignore b/apps/.gitignore
new file mode 100644
index 000000000..c3ae4631e
--- /dev/null
+++ b/apps/.gitignore
@@ -0,0 +1,4 @@
+bin
+gen
+local.properties
+
diff --git a/apps/NotificationStudio/.classpath b/apps/NotificationStudio/.classpath
new file mode 100644
index 000000000..a4763d1ee
--- /dev/null
+++ b/apps/NotificationStudio/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/apps/NotificationStudio/.project b/apps/NotificationStudio/.project
new file mode 100644
index 000000000..00f0a2118
--- /dev/null
+++ b/apps/NotificationStudio/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>NotificationStudio</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/apps/NotificationStudio/.settings/org.eclipse.jdt.core.prefs b/apps/NotificationStudio/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..f77b31c2d
--- /dev/null
+++ b/apps/NotificationStudio/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/apps/NotificationStudio/AndroidManifest.xml b/apps/NotificationStudio/AndroidManifest.xml
new file mode 100644
index 000000000..cb849e006
--- /dev/null
+++ b/apps/NotificationStudio/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<!--
+ Copyright 2012 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.android.notificationstudio"
+ android:versionCode="5"
+ android:versionName="0.5" >
+
+ <uses-sdk android:targetSdkVersion="16" android:minSdkVersion="10" />
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+ <application android:label="@string/app_name" >
+ <activity
+ android:name=".NotificationStudioActivity"
+ 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> \ No newline at end of file
diff --git a/apps/NotificationStudio/libs/android-support-v4.jar b/apps/NotificationStudio/libs/android-support-v4.jar
new file mode 100644
index 000000000..f92dea2aa
--- /dev/null
+++ b/apps/NotificationStudio/libs/android-support-v4.jar
Binary files differ
diff --git a/apps/NotificationStudio/project.properties b/apps/NotificationStudio/project.properties
new file mode 100644
index 000000000..9b84a6b4b
--- /dev/null
+++ b/apps/NotificationStudio/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-16
diff --git a/apps/NotificationStudio/res/drawable-hdpi/android_logo.gif b/apps/NotificationStudio/res/drawable-hdpi/android_logo.gif
new file mode 100644
index 000000000..6078e8632
--- /dev/null
+++ b/apps/NotificationStudio/res/drawable-hdpi/android_logo.gif
Binary files differ
diff --git a/apps/NotificationStudio/res/drawable-hdpi/ic_notification_multiple_mail_holo_dark.png b/apps/NotificationStudio/res/drawable-hdpi/ic_notification_multiple_mail_holo_dark.png
new file mode 100644
index 000000000..c4f3648c2
--- /dev/null
+++ b/apps/NotificationStudio/res/drawable-hdpi/ic_notification_multiple_mail_holo_dark.png
Binary files differ
diff --git a/apps/NotificationStudio/res/drawable-hdpi/romain.jpg b/apps/NotificationStudio/res/drawable-hdpi/romain.jpg
new file mode 100644
index 000000000..e93290199
--- /dev/null
+++ b/apps/NotificationStudio/res/drawable-hdpi/romain.jpg
Binary files differ
diff --git a/apps/NotificationStudio/res/drawable-nodpi/icon_bg.xml b/apps/NotificationStudio/res/drawable-nodpi/icon_bg.xml
new file mode 100644
index 000000000..dcecdd3d2
--- /dev/null
+++ b/apps/NotificationStudio/res/drawable-nodpi/icon_bg.xml
@@ -0,0 +1,29 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <solid android:color="@color/icon_background" />
+ <stroke android:width="1px" android:color="@android:color/holo_blue_light" />
+ </shape>
+ </item>
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="@color/icon_background" />
+ </shape>
+ </item>
+</selector> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/drawable-nodpi/romainguy_rockaway.jpg b/apps/NotificationStudio/res/drawable-nodpi/romainguy_rockaway.jpg
new file mode 100644
index 000000000..68473ba6c
--- /dev/null
+++ b/apps/NotificationStudio/res/drawable-nodpi/romainguy_rockaway.jpg
Binary files differ
diff --git a/apps/NotificationStudio/res/layout-v14/boolean_editor.xml b/apps/NotificationStudio/res/layout-v14/boolean_editor.xml
new file mode 100644
index 000000000..ae7746852
--- /dev/null
+++ b/apps/NotificationStudio/res/layout-v14/boolean_editor.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+ <Switch xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
diff --git a/apps/NotificationStudio/res/layout-v16/studio.xml b/apps/NotificationStudio/res/layout-v16/studio.xml
new file mode 100644
index 000000000..083ac7970
--- /dev/null
+++ b/apps/NotificationStudio/res/layout-v16/studio.xml
@@ -0,0 +1,33 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.android.notificationstudio.MaxHeightScrollView
+ android:id="@+id/preview_scroller"
+ android:layout_width="@dimen/notification_panel_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_marginBottom="5dp" >
+
+ <include layout="@layout/preview" />
+ </com.android.notificationstudio.MaxHeightScrollView>
+
+ <include layout="@layout/editors" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout-w801dp/studio.xml b/apps/NotificationStudio/res/layout-w801dp/studio.xml
new file mode 100644
index 000000000..0391f8ba0
--- /dev/null
+++ b/apps/NotificationStudio/res/layout-w801dp/studio.xml
@@ -0,0 +1,28 @@
+<!--
+ Copyright 2012 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:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <include
+ android:layout_width="@dimen/notification_panel_width"
+ android:layout_height="match_parent"
+ layout="@layout/preview" />
+
+ <include layout="@layout/editors" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout/boolean_editor.xml b/apps/NotificationStudio/res/layout/boolean_editor.xml
new file mode 100644
index 000000000..e4823221b
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/boolean_editor.xml
@@ -0,0 +1,20 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
diff --git a/apps/NotificationStudio/res/layout/divider.xml b/apps/NotificationStudio/res/layout/divider.xml
new file mode 100644
index 000000000..d2e20f8a5
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/divider.xml
@@ -0,0 +1,40 @@
+<!--
+ Copyright 2012 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:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/divider_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/editor_inset"
+ android:layout_marginTop="3dp"
+ android:textAllCaps="true"
+ android:textColor="@color/divider_text"
+ android:textSize="@dimen/editor_text_size" />
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginLeft="@dimen/editor_inset"
+ android:layout_marginRight="@dimen/editor_inset"
+ android:layout_marginTop="3dp"
+ android:background="@color/divider_text" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout/editable_item.xml b/apps/NotificationStudio/res/layout/editable_item.xml
new file mode 100644
index 000000000..816dd0f9a
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/editable_item.xml
@@ -0,0 +1,111 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_item_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/editor_inset" >
+
+ <TextView
+ android:id="@+id/caption"
+ android:layout_width="@dimen/editor_caption_width"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/caption_padding_bottom"
+ android:paddingTop="@dimen/caption_padding_top"
+ android:textSize="@dimen/editor_text_size" />
+
+ <EditText
+ android:id="@+id/text_editor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/caption"
+ android:imeOptions="actionDone"
+ android:inputType="text"
+ android:paddingTop="0dp"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
+
+ <View
+ android:layout_width="0px"
+ android:layout_height="0px"
+ android:focusable="true"
+ android:focusableInTouchMode="true" />
+
+ <ViewStub
+ android:id="@+id/boolean_editor_stub"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/caption"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
+
+ <Spinner
+ android:id="@+id/drop_down_editor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/caption"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:visibility="gone" />
+
+ <HorizontalScrollView
+ android:id="@+id/icon_editor_scroller"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/caption"
+ android:scrollbars="none"
+ android:visibility="gone" >
+
+ <LinearLayout
+ android:id="@+id/icon_editor_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+ </LinearLayout>
+ </HorizontalScrollView>
+
+ <Button
+ android:id="@+id/date_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/caption"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/time_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/date_button"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
+
+ <Button
+ android:id="@+id/reset_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@id/caption"
+ android:layout_toRightOf="@id/time_button"
+ android:text="@string/now"
+ android:textSize="@dimen/editor_text_size"
+ android:visibility="gone" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout/editors.xml b/apps/NotificationStudio/res/layout/editors.xml
new file mode 100644
index 000000000..7c7e12009
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/editors.xml
@@ -0,0 +1,29 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/editors"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/black" >
+
+ <LinearLayout
+ android:id="@+id/items"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+</ScrollView> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout/preview.xml b/apps/NotificationStudio/res/layout/preview.xml
new file mode 100644
index 000000000..3d9c7a082
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/preview.xml
@@ -0,0 +1,56 @@
+<!--
+ Copyright 2012 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/preview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <FrameLayout
+ android:id="@+id/ticker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@+id/large_icon"
+ android:layout_width="@android:dimen/notification_large_icon_width"
+ android:layout_height="@android:dimen/notification_large_icon_height"
+ android:layout_alignParentLeft="true"
+ android:layout_below="@id/ticker"
+ android:scaleType="center"
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+ <FrameLayout
+ android:id="@+id/oneU"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/ticker"
+ android:layout_marginTop="5dp"
+ android:layout_toRightOf="@id/large_icon"
+ android:visibility="gone" />
+
+ <FrameLayout
+ android:id="@+id/fourU"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/oneU"
+ android:layout_marginTop="5dp"
+ android:visibility="gone" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/layout/studio.xml b/apps/NotificationStudio/res/layout/studio.xml
new file mode 100644
index 000000000..2646a44d3
--- /dev/null
+++ b/apps/NotificationStudio/res/layout/studio.xml
@@ -0,0 +1,26 @@
+<!--
+ Copyright 2012 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:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <include layout="@layout/preview" />
+
+ <include layout="@layout/editors" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/menu/action_bar.xml b/apps/NotificationStudio/res/menu/action_bar.xml
new file mode 100644
index 000000000..35c897fe6
--- /dev/null
+++ b/apps/NotificationStudio/res/menu/action_bar.xml
@@ -0,0 +1,30 @@
+<!--
+ Copyright 2012 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.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/action_share_code"
+ android:icon="@android:drawable/ic_menu_share"
+ android:showAsAction="never"
+ android:title="@string/share_generated_code"/>
+ <item
+ android:id="@+id/action_share_mockup"
+ android:icon="@android:drawable/ic_menu_share"
+ android:showAsAction="never"
+ android:title="@string/share_mockup"/>
+
+</menu> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values-sw600dp/dimens.xml b/apps/NotificationStudio/res/values-sw600dp/dimens.xml
new file mode 100644
index 000000000..22e863f3c
--- /dev/null
+++ b/apps/NotificationStudio/res/values-sw600dp/dimens.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright 2012 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>
+
+ <dimen name="notification_panel_width">446dp</dimen>
+ <dimen name="caption_padding_bottom">6dp</dimen>
+ <dimen name="editor_text_size">16dp</dimen>
+ <dimen name="editor_caption_width">110dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values-sw720dp/dimens.xml b/apps/NotificationStudio/res/values-sw720dp/dimens.xml
new file mode 100644
index 000000000..f8960473d
--- /dev/null
+++ b/apps/NotificationStudio/res/values-sw720dp/dimens.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2012 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>
+
+ <dimen name="notification_panel_width">448dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values-v11/colors.xml b/apps/NotificationStudio/res/values-v11/colors.xml
new file mode 100644
index 000000000..224b09e70
--- /dev/null
+++ b/apps/NotificationStudio/res/values-v11/colors.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2012 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>
+
+ <color name="divider_text">@android:color/holo_blue_bright</color>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values-v11/dimens.xml b/apps/NotificationStudio/res/values-v11/dimens.xml
new file mode 100644
index 000000000..f5fd098fb
--- /dev/null
+++ b/apps/NotificationStudio/res/values-v11/dimens.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright 2012 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>
+
+ <dimen name="caption_padding_top">6dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values/colors.xml b/apps/NotificationStudio/res/values/colors.xml
new file mode 100644
index 000000000..aabe9bc25
--- /dev/null
+++ b/apps/NotificationStudio/res/values/colors.xml
@@ -0,0 +1,23 @@
+<!--
+ Copyright 2012 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>
+
+ <color name="gb_background">#dddddd</color>
+ <color name="divider_text">#dddddd</color>
+ <color name="icon_background">#3333B5E5</color>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values/dimens.xml b/apps/NotificationStudio/res/values/dimens.xml
new file mode 100644
index 000000000..e960ae6f6
--- /dev/null
+++ b/apps/NotificationStudio/res/values/dimens.xml
@@ -0,0 +1,33 @@
+<!--
+ Copyright 2012 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>
+
+ <dimen name="notification_panel_width">-1dp</dimen> <!-- MATCH_PARENT -->
+ <dimen name="editor_inset">8dp</dimen>
+ <dimen name="caption_padding_top">10dp</dimen>
+ <dimen name="caption_padding_bottom">0dp</dimen>
+ <dimen name="editor_text_size">12dp</dimen>
+ <dimen name="editor_caption_width">95dp</dimen>
+ <dimen name="editor_icon_size_small">40dp</dimen>
+ <dimen name="editor_icon_size_large">60dp</dimen>
+ <dimen name="editor_icon_outer_margin">5dp</dimen>
+ <dimen name="editor_icon_inner_margin">2dp</dimen>
+ <dimen name="editor_drop_down_padding">5dp</dimen>
+ <dimen name="editor_datetime_padding_v">0dp</dimen>
+ <dimen name="editor_datetime_padding_h">15dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/res/values/strings.xml b/apps/NotificationStudio/res/values/strings.xml
new file mode 100644
index 000000000..e3990f3d9
--- /dev/null
+++ b/apps/NotificationStudio/res/values/strings.xml
@@ -0,0 +1,55 @@
+<!--
+ Copyright 2012 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="app_name">Notification Studio</string>
+ <string name="now">Now</string>
+ <string name="share_generated_code">Share generated code…</string>
+ <string name="share_mockup">Share mockup…</string>
+ <string name="preset">Preset</string>
+ <string name="small_icon">Small Icon</string>
+ <string name="content_title">Content Title</string>
+ <string name="content_text">Content Text</string>
+ <string name="sub_text">Sub Text</string>
+ <string name="large_icon">Large Icon</string>
+ <string name="content_info">Content Info</string>
+ <string name="number">Number</string>
+ <string name="when">When</string>
+ <string name="progress">Progress</string>
+ <string name="uses_chron">Uses Chron</string>
+ <string name="style">Style</string>
+ <string name="picture">Picture</string>
+ <string name="big_text">Big Text</string>
+ <string name="lines">Lines</string>
+ <string name="big_content_title">Big Content Title</string>
+ <string name="summary_text">Summary Text</string>
+ <string name="icon">Icon</string>
+ <string name="text">Text</string>
+ <string name="properties">Properties</string>
+ <string name="action_1">Action 1</string>
+ <string name="action_2">Action 2</string>
+ <string name="action_3">Action 3</string>
+ <string name="preset_custom">(custom)</string>
+ <string name="preset_basic">Basic example</string>
+ <string name="preset_email">Email example</string>
+ <string name="preset_photo">Photo example</string>
+ <string name="style_none">(none)</string>
+ <string name="style_big_picture">BigPictureStyle</string>
+ <string name="style_big_text">BigTextStyle</string>
+ <string name="style_inbox">InboxStyle</string>
+
+</resources> \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/MaxHeightScrollView.java b/apps/NotificationStudio/src/com/android/notificationstudio/MaxHeightScrollView.java
new file mode 100644
index 000000000..1fc0284b1
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/MaxHeightScrollView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2012 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.notificationstudio;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ScrollView;
+
+public class MaxHeightScrollView extends ScrollView {
+
+ private int mMaxHeight;
+
+ public MaxHeightScrollView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setMaxHeight(int maxHeight) {
+ mMaxHeight = maxHeight;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(getMeasuredWidth(), Math.min(getMeasuredHeight(), mMaxHeight));
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/NotificationStudioActivity.java b/apps/NotificationStudio/src/com/android/notificationstudio/NotificationStudioActivity.java
new file mode 100644
index 000000000..9c2e8119a
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/NotificationStudioActivity.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2012 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.notificationstudio;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RemoteViews;
+import android.widget.TextView;
+
+import com.android.notificationstudio.action.ShareCodeAction;
+import com.android.notificationstudio.action.ShareMockupAction;
+import com.android.notificationstudio.editor.Editors;
+import com.android.notificationstudio.generator.NotificationGenerator;
+import com.android.notificationstudio.model.EditableItem;
+import com.android.notificationstudio.model.EditableItemConstants;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class NotificationStudioActivity extends Activity implements EditableItemConstants{
+ private static final String TAG = NotificationStudioActivity.class.getSimpleName();
+ private static final int PREVIEW_NOTIFICATION = 1;
+ private static final int REFRESH_DELAY = 50;
+ private static final ExecutorService BACKGROUND = Executors.newSingleThreadExecutor();
+
+ private boolean mRefreshPending;
+
+ private final Handler mHandler = new Handler();
+ private final Runnable mRefreshNotificationInner = new Runnable() {
+ public void run() {
+ refreshNotificationInner();
+ }};
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().setBackgroundDrawableResource(android.R.color.black);
+ setContentView(R.layout.studio);
+ initPreviewScroller();
+
+ EditableItem.initIfNecessary(this);
+
+ initEditors();
+ }
+
+ private void initPreviewScroller() {
+
+ MaxHeightScrollView preview = (MaxHeightScrollView) findViewById(R.id.preview_scroller);
+ if (preview == null)
+ return;
+ final int margin = ((ViewGroup.MarginLayoutParams) preview.getLayoutParams()).bottomMargin;
+ preview.addOnLayoutChangeListener(new OnLayoutChangeListener(){
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ // animate preview height changes
+ if (oldBottom != bottom) {
+ final View e = findViewById(R.id.editors);
+ final int y = bottom + margin;
+ e.animate()
+ .translationY(y - oldBottom)
+ .setListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animation) {
+ FrameLayout.LayoutParams lp = (LayoutParams) e.getLayoutParams();
+ lp.topMargin = y;
+ e.setTranslationY(0);
+ e.setLayoutParams(lp);
+ }
+ });
+ }
+ }});
+
+ // limit the max height for preview, leave room for editors + soft keyboard
+ DisplayMetrics dm = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(dm);
+ float actualHeight = dm.heightPixels / dm.ydpi;
+ float pct = actualHeight < 3.5 ? .32f :
+ actualHeight < 4 ? .35f :
+ .38f;
+ preview.setMaxHeight((int)(dm.heightPixels * pct));
+ }
+
+ private void initEditors() {
+ LinearLayout items = (LinearLayout) findViewById(R.id.items);
+ items.removeAllViews();
+ String currentCategory = null;
+ for (EditableItem item : EditableItem.values()) {
+ String itemCategory = item.getCategory(this);
+ if (!itemCategory.equals(currentCategory)) {
+ View dividerView = getLayoutInflater().inflate(R.layout.divider, null);
+ ((TextView) dividerView.findViewById(R.id.divider_text)).setText(itemCategory);
+ items.addView(dividerView);
+ currentCategory = itemCategory;
+ }
+ View editorView = Editors.newEditor(this, items, item);
+ if (editorView != null)
+ items.addView(editorView);
+ }
+ refreshNotification();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ // we'll take care of restoring state
+ }
+
+ public void refreshNotification() {
+ mRefreshPending = true;
+ mHandler.postDelayed(mRefreshNotificationInner, REFRESH_DELAY);
+ }
+
+ private void refreshNotificationInner() {
+ if (!mRefreshPending) {
+ return;
+ }
+ final Notification notification = NotificationGenerator.build(this);
+ ViewGroup oneU = (ViewGroup) findViewById(R.id.oneU);
+ ViewGroup fourU = (ViewGroup) findViewById(R.id.fourU);
+ View oneUView = refreshRemoteViews(oneU, notification.contentView);
+ if (Build.VERSION.SDK_INT >= 16)
+ refreshRemoteViews(fourU, notification.bigContentView);
+ else if (Build.VERSION.SDK_INT >= 11) {
+ ImageView largeIcon = (ImageView) findViewById(R.id.large_icon);
+ largeIcon.setVisibility(notification.largeIcon == null ? View.GONE : View.VISIBLE);
+ if (notification.largeIcon != null)
+ largeIcon.setImageBitmap(notification.largeIcon);
+ } else if (oneUView != null) {
+ oneUView.setBackgroundColor(getResources().getColor(R.color.gb_background));
+ oneUView.setMinimumHeight(100);
+ }
+ mRefreshPending = false;
+
+ // this can take a while, run on a background thread
+ BACKGROUND.submit(new Runnable() {
+ public void run() {
+ NotificationManager mgr =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ try {
+ mgr.notify(PREVIEW_NOTIFICATION, notification);
+ } catch (Throwable t) {
+ Log.w(TAG, "Error displaying notification", t);
+ }
+ }});
+ }
+
+ private View refreshRemoteViews(ViewGroup parent, RemoteViews remoteViews) {
+ parent.removeAllViews();
+ if (remoteViews != null) {
+ parent.setVisibility(View.VISIBLE);
+ try {
+ View v = remoteViews.apply(this, parent);
+ parent.addView(v);
+ return v;
+ } catch (Exception e) {
+ TextView exceptionView = new TextView(this);
+ exceptionView.setText(e.getClass().getSimpleName() + ": " + e.getMessage());
+ parent.addView(exceptionView);
+ return exceptionView;
+ }
+ } else {
+ parent.setVisibility(View.GONE);
+ return null;
+ }
+ }
+
+ // action bar setup
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.action_bar, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_share_code:
+ ShareCodeAction.launch(this, item.getTitle());
+ return true;
+ case R.id.action_share_mockup:
+ ShareMockupAction.launch(this, item.getTitle());
+ return true;
+ }
+ return false;
+ }
+
+ // hides the soft keyboard more aggressively when leaving text editors
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ View v = getCurrentFocus();
+ boolean ret = super.dispatchTouchEvent(event);
+
+ if (v instanceof EditText) {
+ View currentFocus = getCurrentFocus();
+ int screenCoords[] = new int[2];
+ currentFocus.getLocationOnScreen(screenCoords);
+ float x = event.getRawX() + currentFocus.getLeft() - screenCoords[0];
+ float y = event.getRawY() + currentFocus.getTop() - screenCoords[1];
+
+ if (event.getAction() == MotionEvent.ACTION_UP
+ && (x < currentFocus.getLeft() ||
+ x >= currentFocus.getRight() ||
+ y < currentFocus.getTop() ||
+ y > currentFocus.getBottom())) {
+ InputMethodManager imm =
+ (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(getWindow().getCurrentFocus().getWindowToken(), 0);
+ }
+ }
+ return ret;
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareCodeAction.java b/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareCodeAction.java
new file mode 100644
index 000000000..d7da43446
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareCodeAction.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012 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.notificationstudio.action;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.notificationstudio.generator.CodeGenerator;
+
+public class ShareCodeAction {
+
+ public static void launch(Context context, CharSequence title) {
+ String shareBody = CodeGenerator.generate(context);
+ Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+ sharingIntent.setType("text/plain");
+ sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Notification Code");
+ sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, shareBody);
+ context.startActivity(Intent.createChooser(sharingIntent, title));
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareMockupAction.java b/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareMockupAction.java
new file mode 100644
index 000000000..e377475c1
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/action/ShareMockupAction.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2012 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.notificationstudio.action;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.net.Uri;
+import android.util.Log;
+import android.view.View;
+import android.widget.Toast;
+
+import com.android.notificationstudio.R;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class ShareMockupAction {
+ private static final String TAG = ShareMockupAction.class.getSimpleName();
+
+ private static final SimpleDateFormat FILE_NAME =
+ new SimpleDateFormat("'notification.'yyyyMMdd'.'HHmmss'.png'");
+
+ public static void launch(Activity activity, CharSequence title) {
+ // take a picture of the current mockup
+ View v = activity.findViewById(R.id.preview);
+ int w = v.getMeasuredWidth();
+ int h = v.getMeasuredHeight();
+ Bitmap mockup = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(mockup);
+ v.layout(0, 0, w, h);
+ v.draw(c);
+
+ // write the mockup to a temp file
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ mockup.compress(Bitmap.CompressFormat.PNG, 100, bytes);
+ File f = new File(activity.getExternalCacheDir(), FILE_NAME.format(new Date()));
+ FileOutputStream fo = null;
+ try {
+ f.createNewFile();
+ fo = new FileOutputStream(f);
+ fo.write(bytes.toByteArray());
+ } catch (IOException e) {
+ String msg = "Error writing mockup file";
+ Log.w(TAG, msg, e);
+ Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
+ return;
+ } finally {
+ if (fo != null)
+ try { fo.close(); } catch (Exception e) { }
+ }
+
+ // launch intent to send the mockup image
+ Intent share = new Intent(Intent.ACTION_SEND);
+ share.setType("image/png");
+ share.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f.getAbsoluteFile()));
+ activity.startActivity(Intent.createChooser(share, title));
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/BitmapEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/BitmapEditor.java
new file mode 100644
index 000000000..04194acc3
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/BitmapEditor.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.widget.ImageView;
+
+import com.android.notificationstudio.R;
+
+public class BitmapEditor extends IconEditor {
+
+ @Override
+ protected void setImage(ImageView imageView, Object value) {
+ imageView.setImageBitmap((Bitmap) value);
+ }
+
+ protected int getIconSize(Resources res) {
+ return res.getDimensionPixelSize(R.dimen.editor_icon_size_large);
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/BooleanEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/BooleanEditor.java
new file mode 100644
index 000000000..6a6a8cfbd
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/BooleanEditor.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.view.View;
+import android.view.ViewStub;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.Switch;
+
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.editor.Editors.Editor;
+import com.android.notificationstudio.model.EditableItem;
+
+public class BooleanEditor implements Editor {
+
+ public Runnable bindEditor(View v, final EditableItem item, final Runnable afterChange) {
+ final ViewStub booleanEditorStub = (ViewStub) v.findViewById(R.id.boolean_editor_stub);
+ booleanEditorStub.setLayoutResource(R.layout.boolean_editor);
+ final Switch booleanEditor = (Switch) booleanEditorStub.inflate();
+ Runnable updateSwitch = new Runnable() {
+ public void run() {
+ booleanEditor.setChecked(item.hasValue() && item.getValueBool());
+ }};
+ booleanEditor.setVisibility(View.VISIBLE);
+ updateSwitch.run();
+ booleanEditor.setOnCheckedChangeListener(new OnCheckedChangeListener(){
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ item.setValue(isChecked);
+ afterChange.run();
+ }});
+ return updateSwitch;
+ }
+
+} \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/DateTimeEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/DateTimeEditor.java
new file mode 100644
index 000000000..8089de139
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/DateTimeEditor.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.app.Activity;
+import android.app.DatePickerDialog;
+import android.app.DatePickerDialog.OnDateSetListener;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentTransaction;
+import android.app.TimePickerDialog;
+import android.app.TimePickerDialog.OnTimeSetListener;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.TimePicker;
+
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.editor.Editors.Editor;
+import com.android.notificationstudio.model.EditableItem;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class DateTimeEditor implements Editor {
+ private static final SimpleDateFormat YYYY_MM_DD = new SimpleDateFormat("yyyy/MM/dd");
+ private static final SimpleDateFormat HH_MM_SS = new SimpleDateFormat("HH:mm:ss");
+
+ @SuppressWarnings("deprecation")
+ public Runnable bindEditor(View v, final EditableItem item, final Runnable afterChange) {
+
+ final Button dateButton = (Button) v.findViewById(R.id.date_button);
+ final Button timeButton = (Button) v.findViewById(R.id.time_button);
+ final Button resetButton = (Button) v.findViewById(R.id.reset_button);
+
+ int vPad = v.getResources().getDimensionPixelSize(R.dimen.editor_datetime_padding_v);
+ int hPad = v.getResources().getDimensionPixelSize(R.dimen.editor_datetime_padding_h);
+ for (Button b : new Button[] { dateButton, timeButton, resetButton }) {
+ b.setVisibility(View.VISIBLE);
+ b.setPadding(hPad, vPad, hPad, vPad);
+ }
+
+ final Runnable updateButtonText = new Runnable() {
+ public void run() {
+ Date d = getDateTime(item);
+ String dateString = YYYY_MM_DD.format(d);
+ dateButton.setText(dateString);
+ String timeString = HH_MM_SS.format(d);
+ timeButton.setText(timeString);
+ }};
+ updateButtonText.run();
+
+ // wire up date button
+ DialogFragment datePickerFragment = new DialogFragment() {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Date d = getDateTime(item);
+ OnDateSetListener onDateSet = new OnDateSetListener() {
+ public void onDateSet(DatePicker view, int year,
+ int monthOfYear, int dayOfMonth) {
+ Date d = getDateTime(item);
+ d.setYear(year - 1900);
+ d.setMonth(monthOfYear);
+ d.setDate(dayOfMonth);
+ item.setValue(d.getTime());
+ updateButtonText.run();
+ afterChange.run();
+ }
+ };
+ return new DatePickerDialog(getActivity(), onDateSet,
+ d.getYear() + 1900, d.getMonth(), d.getDate());
+ }
+ };
+ Activity activity = (Activity) v.getContext();
+ launchDialogOnClick(activity, "datePicker", dateButton, datePickerFragment);
+
+ // wire up time button
+ DialogFragment timePickerFragment = new DialogFragment() {
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ Date d = getDateTime(item);
+ OnTimeSetListener onTimeSet = new OnTimeSetListener() {
+ public void onTimeSet(TimePicker view, int hourOfDay,
+ int minute) {
+ Date d = getDateTime(item);
+ d.setHours(hourOfDay);
+ d.setMinutes(minute);
+ item.setValue(d.getTime());
+ updateButtonText.run();
+ afterChange.run();
+ }
+ };
+ return new TimePickerDialog(getActivity(),
+ onTimeSet, d.getHours(), d.getMinutes(), true);
+ }
+ };
+ launchDialogOnClick(activity, "timePicker", timeButton, timePickerFragment);
+
+ // wire up reset button
+ resetButton.setOnClickListener(new OnClickListener(){
+ public void onClick(View v) {
+ item.setValue(null);
+ updateButtonText.run();
+ afterChange.run();
+ }});
+ return updateButtonText;
+ }
+
+ private static Date getDateTime(EditableItem item) {
+ long value = item.hasValue() ? item.getValueLong() : System.currentTimeMillis();
+ return new Date(value);
+ }
+
+ private static void launchDialogOnClick(final Activity activity,
+ final String tag, Button button, final DialogFragment fragment) {
+ button.setOnClickListener(new OnClickListener(){
+ public void onClick(View v) {
+ FragmentTransaction ft = activity.getFragmentManager().beginTransaction();
+ fragment.show(ft, tag);
+ }});
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/DropDownEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/DropDownEditor.java
new file mode 100644
index 000000000..64490fafe
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/DropDownEditor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.editor.Editors.Editor;
+import com.android.notificationstudio.model.EditableItem;
+
+public class DropDownEditor implements Editor {
+
+ public Runnable bindEditor(View v, final EditableItem item, final Runnable afterChange) {
+ final Spinner dropDownEditor = (Spinner) v.findViewById(R.id.drop_down_editor);
+ dropDownEditor.setVisibility(View.VISIBLE);
+ Integer[] values = item.getAvailableValuesInteger();
+ final float textSize = v.getResources().getDimensionPixelSize(R.dimen.editor_text_size);
+ final int p = v.getResources().getDimensionPixelSize(R.dimen.editor_drop_down_padding);
+ final int p2 = p * 2;
+ final ArrayAdapter<Integer> adapter =
+ new ArrayAdapter<Integer>(v.getContext(), android.R.layout.simple_spinner_item, values) {
+
+ @Override
+ public View getDropDownView(int position, View convertView, ViewGroup parent) {
+ TextView v = (TextView) super.getDropDownView(position, convertView, parent);
+ v.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+ v.setPadding(p2, p2, p2, p2);
+ v.setText(v.getResources().getString(getItem(position)));
+ return v;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v = (TextView) super.getView(position, convertView, parent);
+ v.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+ v.setPadding(p, p, p, p);
+ v.setText(v.getResources().getString(getItem(position)));
+ return v;
+ }
+ };
+ dropDownEditor.setAdapter(adapter);
+ Runnable updateSelection = new Runnable() {
+ public void run() {
+ dropDownEditor.setSelection(adapter.getPosition(item.getValueInt()));
+ }};
+ if (item.hasValue())
+ updateSelection.run();
+ dropDownEditor.setOnItemSelectedListener(new OnItemSelectedListener(){
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ Object oldValue = item.getValue();
+ Object newValue = adapter.getItem(position);
+ if (newValue.equals(oldValue))
+ return;
+ item.setValue(newValue);
+ afterChange.run();
+ }
+ public void onNothingSelected(AdapterView<?> parent) {
+ // noop
+ }});
+ return updateSelection;
+ }
+
+} \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/Editors.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/Editors.java
new file mode 100644
index 000000000..b7268f111
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/Editors.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.notificationstudio.NotificationStudioActivity;
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.model.EditableItem;
+import com.android.notificationstudio.model.EditableItemConstants;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Editors implements EditableItemConstants {
+
+ public interface Editor {
+ Runnable bindEditor(View v, EditableItem item, Runnable afterChange);
+ }
+
+ private static final Map<Integer, Editor> EDITORS = editors();
+ private static Runnable sUpdatePreset;
+
+ private static Map<Integer, Editor> editors() {
+ Map<Integer, Editor> editors = new HashMap<Integer, Editor>();
+ editors.put(TYPE_RESOURCE_ID, new IconEditor());
+ editors.put(TYPE_TEXT, new TextEditor());
+ editors.put(TYPE_INT, new IntEditor());
+ if (Build.VERSION.SDK_INT >= 14) // switch 14, progress 14, uses chron 16
+ editors.put(TYPE_BOOLEAN, new BooleanEditor());
+ editors.put(TYPE_DROP_DOWN, new DropDownEditor());
+ editors.put(TYPE_BITMAP, new BitmapEditor());
+ if (Build.VERSION.SDK_INT >= 11) // fragments 11, when 11
+ editors.put(TYPE_DATETIME, new DateTimeEditor());
+ editors.put(TYPE_TEXT_LINES, new LinesEditor());
+ return editors;
+ }
+
+ public static View newEditor(final NotificationStudioActivity activity,
+ final ViewGroup parent, final EditableItem item) {
+ final View editorView = activity.getLayoutInflater().inflate(R.layout.editable_item, null);
+ ((TextView) editorView.findViewById(R.id.caption)).setText(item.getCaption(activity));
+
+ // bind visibility
+ editorView.setVisibility(item.isVisible() ? View.VISIBLE : View.GONE);
+ item.setVisibilityListener(new Runnable(){
+ public void run() {
+ editorView.setVisibility(item.isVisible() ? View.VISIBLE : View.GONE);
+ }});
+
+ // bind type-specific behavior
+ Editor editor = EDITORS.get(item.getType());
+ if (editor == null)
+ return null;
+ Runnable updater = editor.bindEditor(editorView, item, new Runnable() {
+ public void run() {
+ if (item.equals(EditableItem.PRESET)) {
+ updateEditors(parent);
+ } else {
+ EditableItem.PRESET.setValue(PRESET_CUSTOM);
+ sUpdatePreset.run();
+ }
+ activity.refreshNotification();
+ }});
+
+ // store the updater as the view tag
+ editorView.setTag(updater);
+ if (item.equals(EditableItem.PRESET))
+ sUpdatePreset = updater;
+
+ return editorView;
+ }
+
+ private static void updateEditors(ViewGroup parent) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ Object childTag = parent.getChildAt(i).getTag();
+ if (childTag instanceof Runnable) {
+ ((Runnable) childTag).run();
+ }
+ }
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/IconEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/IconEditor.java
new file mode 100644
index 000000000..71e900567
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/IconEditor.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.content.res.Resources;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.editor.Editors.Editor;
+import com.android.notificationstudio.model.EditableItem;
+
+public class IconEditor implements Editor {
+
+ public Runnable bindEditor(View v, final EditableItem item, final Runnable afterChange) {
+ final LinearLayout iconEditor = (LinearLayout) v.findViewById(R.id.icon_editor_layout);
+ final HorizontalScrollView scroller =
+ (HorizontalScrollView) v.findViewById(R.id.icon_editor_scroller);
+ scroller.setVisibility(View.VISIBLE);
+
+ final Object[] displayValues = getAvailableValuesForDisplay(item);
+ final Runnable updateSelection = new Runnable() {
+ public void run() {
+ for (int i=0;i<iconEditor.getChildCount();i++) {
+ View imageViewHolder = iconEditor.getChildAt(i);
+ Object iconResId = imageViewHolder.getTag();
+ boolean selected = item.hasValue() && item.getValue().equals(iconResId) ||
+ !item.hasValue() && iconResId == null;
+ imageViewHolder.setSelected(selected);
+ if (selected) {
+ int x = imageViewHolder.getLeft();
+ if (x < scroller.getScrollX() ||
+ x > scroller.getScrollX() + scroller.getWidth()) {
+ scroller.scrollTo(imageViewHolder.getLeft(), 0);
+ }
+ }
+ }
+ }};
+
+ int iconSize = getIconSize(v.getResources());
+ int outerMargin = v.getResources().getDimensionPixelSize(R.dimen.editor_icon_outer_margin);
+ int innerMargin = v.getResources().getDimensionPixelSize(R.dimen.editor_icon_inner_margin);
+
+ for (final Object iconResId : displayValues) {
+ final FrameLayout imageViewHolder = new FrameLayout(v.getContext());
+ imageViewHolder.setTag(iconResId);
+ final ImageView imageView = new ImageView(v.getContext());
+ imageView.setScaleType(ScaleType.CENTER);
+ imageView.setOnTouchListener(new OnTouchListener(){
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ v.playSoundEffect(SoundEffectConstants.CLICK);
+ item.setValue(iconResId);
+ updateSelection.run();
+ afterChange.run();
+ }
+ return true;
+ }});
+
+ imageViewHolder.setBackgroundResource(R.drawable.icon_bg);
+ if (iconResId != null)
+ setImage(imageView, iconResId);
+
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(iconSize, iconSize);
+ lp.bottomMargin = lp.topMargin = lp.leftMargin = lp.rightMargin = outerMargin;
+ imageViewHolder.setLayoutParams(lp);
+
+ FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT);
+ flp.bottomMargin = flp.topMargin = flp.leftMargin = flp.rightMargin = innerMargin;
+ imageView.setLayoutParams(flp);
+
+ imageViewHolder.addView(imageView);
+ iconEditor.addView(imageViewHolder);
+ }
+ updateSelection.run();
+ return updateSelection;
+ }
+
+ protected int getIconSize(Resources res) {
+ return res.getDimensionPixelSize(R.dimen.editor_icon_size_small);
+ }
+
+ protected void setImage(ImageView imageView, Object value) {
+ imageView.setImageResource((Integer) value);
+ }
+
+ private Object[] getAvailableValuesForDisplay(EditableItem item) {
+ Object[] avail = item.getAvailableValues();
+ Object[] rt = new Object[avail.length + 1];
+ System.arraycopy(avail, 0, rt, 1, avail.length);
+ return rt;
+ }
+
+} \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/IntEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/IntEditor.java
new file mode 100644
index 000000000..393863eb1
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/IntEditor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.text.InputType;
+
+public class IntEditor extends TextEditor {
+
+ @Override
+ protected Object parseValue(String str) {
+ return str == null ? null : Integer.parseInt(str);
+ }
+
+ protected int getInputType() {
+ return InputType.TYPE_CLASS_NUMBER;
+ }
+
+} \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/LinesEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/LinesEditor.java
new file mode 100644
index 000000000..5173a992f
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/LinesEditor.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.text.InputType;
+
+public class LinesEditor extends TextEditor {
+
+ @Override
+ protected int getInputType() {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE;
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/editor/TextEditor.java b/apps/NotificationStudio/src/com/android/notificationstudio/editor/TextEditor.java
new file mode 100644
index 000000000..aa38eec34
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/editor/TextEditor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2012 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.notificationstudio.editor;
+
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.EditText;
+
+import com.android.notificationstudio.R;
+import com.android.notificationstudio.editor.Editors.Editor;
+import com.android.notificationstudio.model.EditableItem;
+
+public class TextEditor implements Editor {
+
+ public Runnable bindEditor(View v, final EditableItem item, final Runnable afterChange) {
+ final EditText textEditor = (EditText) v.findViewById(R.id.text_editor);
+ textEditor.setVisibility(View.VISIBLE);
+ textEditor.setInputType(getInputType());
+ Runnable updateEditText = new Runnable() {
+ public void run() {
+ textEditor.setText(item.getValue() == null ? "" : item.getValue().toString());
+ }};
+ if (item.hasValue())
+ updateEditText.run();
+ textEditor.addTextChangedListener(new TextWatcher() {
+ public void afterTextChanged(Editable s) {
+ Object newVal = parseValue(s.length() == 0 ? null : s.toString());
+ if (equal(newVal, item.getValue()))
+ return;
+ item.setValue(newVal);
+ afterChange.run();
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // noop
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // noop
+ }
+ });
+ return updateEditText;
+ }
+
+ protected int getInputType() {
+ return InputType.TYPE_CLASS_TEXT;
+ }
+
+ protected Object parseValue(String str) {
+ return str;
+ }
+
+ private static boolean equal(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+} \ No newline at end of file
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/generator/CodeGenerator.java b/apps/NotificationStudio/src/com/android/notificationstudio/generator/CodeGenerator.java
new file mode 100644
index 000000000..81eca8319
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/generator/CodeGenerator.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2012 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.notificationstudio.generator;
+import static com.android.notificationstudio.model.EditableItem.ACTION1_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION1_TEXT;
+import static com.android.notificationstudio.model.EditableItem.ACTION2_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION2_TEXT;
+import static com.android.notificationstudio.model.EditableItem.ACTION3_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION3_TEXT;
+import static com.android.notificationstudio.model.EditableItem.BIG_CONTENT_TITLE;
+import static com.android.notificationstudio.model.EditableItem.BIG_TEXT;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_INFO;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_TEXT;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_TITLE;
+import static com.android.notificationstudio.model.EditableItem.LARGE_ICON;
+import static com.android.notificationstudio.model.EditableItem.LINES;
+import static com.android.notificationstudio.model.EditableItem.NUMBER;
+import static com.android.notificationstudio.model.EditableItem.PICTURE;
+import static com.android.notificationstudio.model.EditableItem.PROGRESS;
+import static com.android.notificationstudio.model.EditableItem.SMALL_ICON;
+import static com.android.notificationstudio.model.EditableItem.STYLE;
+import static com.android.notificationstudio.model.EditableItem.SUB_TEXT;
+import static com.android.notificationstudio.model.EditableItem.SUMMARY_TEXT;
+import static com.android.notificationstudio.model.EditableItem.USES_CHRON;
+import static com.android.notificationstudio.model.EditableItem.WHEN;
+
+import android.content.Context;
+
+import com.android.notificationstudio.model.EditableItem;
+import com.android.notificationstudio.model.EditableItemConstants;
+
+public class CodeGenerator implements EditableItemConstants {
+
+ private static final String INDENT = "\n ";
+ private static final String STYLE_INDENT = INDENT + " ";
+
+ public static String generate(Context context) {
+
+ StringBuilder sb = new StringBuilder("new Notification.Builder(context)");
+
+ if (SMALL_ICON.hasValue())
+ sb.append(INDENT + ".setSmallIcon(" + getResourceVar(context, SMALL_ICON) + ")");
+ if (CONTENT_TITLE.hasValue())
+ sb.append(INDENT + ".setContentTitle(" + quote(CONTENT_TITLE) + ")");
+ if (CONTENT_TEXT.hasValue())
+ sb.append(INDENT + ".setContentText(" + quote(CONTENT_TEXT) + ")");
+ if (SUB_TEXT.hasValue())
+ sb.append(INDENT + ".setSubText(" + quote(SUB_TEXT) + ")");
+ if (LARGE_ICON.hasValue())
+ sb.append(INDENT + ".setLargeIcon(largeIconBitmap)");
+ if (CONTENT_INFO.hasValue())
+ sb.append(INDENT + ".setContentInfo(" + quote(CONTENT_INFO) + ")");
+ if (NUMBER.hasValue())
+ sb.append(INDENT + ".setNumber(" + NUMBER.getValueInt() + ")");
+ if (WHEN.hasValue())
+ sb.append(INDENT + ".setWhen(" + WHEN.getValueLong() + ")");
+ if (PROGRESS.hasValue() && PROGRESS.getValueBool())
+ sb.append(INDENT + ".setProgress(0, 0, true)");
+ if (USES_CHRON.hasValue())
+ sb.append(INDENT + ".setUsesChronometer(" + USES_CHRON.getValueBool() + ")");
+ if (ACTION1_ICON.hasValue())
+ generateAction(sb, ACTION1_ICON, ACTION1_TEXT, "action1PendingIntent");
+ if (ACTION2_ICON.hasValue())
+ generateAction(sb, ACTION2_ICON, ACTION2_TEXT, "action2PendingIntent");
+ if (ACTION3_ICON.hasValue())
+ generateAction(sb, ACTION3_ICON, ACTION3_TEXT, "action3PendingIntent");
+
+ if (STYLE.hasValue())
+ generateStyle(sb);
+
+ sb.append(INDENT + ".build();");
+ return sb.toString();
+ }
+
+ private static void generateStyle(StringBuilder sb) {
+ Integer styleValue = STYLE.getValueInt();
+ if (STYLE_BIG_PICTURE.equals(styleValue)) {
+ sb.append(INDENT + ".setStyle(new Notification.BigPictureStyle()");
+ if (PICTURE.hasValue())
+ sb.append(STYLE_INDENT + ".bigPicture(pictureBitmap)");
+ }
+ if (STYLE_BIG_TEXT.equals(styleValue)) {
+ sb.append(INDENT + ".setStyle(new Notification.BigTextStyle()");
+ if (BIG_TEXT.hasValue())
+ sb.append(STYLE_INDENT + ".bigText(" + quote(BIG_TEXT) + ")");
+ }
+ if (STYLE_INBOX.equals(styleValue)) {
+ sb.append(INDENT + ".setStyle(new Notification.InboxStyle()");
+ if (LINES.hasValue()) {
+ for (String line : LINES.getValueString().split("\\n")) {
+ sb.append(STYLE_INDENT + ".addLine(" + quote(line) + ")");
+ }
+ }
+ }
+ if (BIG_CONTENT_TITLE.hasValue())
+ sb.append(STYLE_INDENT + ".setBigContentTitle(" + quote(BIG_CONTENT_TITLE) + ")");
+ if (SUMMARY_TEXT.hasValue())
+ sb.append(STYLE_INDENT + ".setSummaryText(" + quote(SUMMARY_TEXT) + ")");
+
+ sb.append(")");
+ }
+
+ private static void generateAction(StringBuilder sb,
+ EditableItem icon, EditableItem text, String intentName) {
+ sb.append(INDENT +
+ ".addAction(" + icon.getValueInt() + ", " + quote(text) + ", " + intentName + ")");
+ }
+
+ private static String quote(EditableItem text) {
+ return quote(text.getValueString());
+ }
+
+ private static String quote(String text) {
+ return text != null ? "\"" + text.replace("\"", "\\\"") + "\"" : "null";
+ }
+
+ private static String getResourceVar(Context context, EditableItem item) {
+ int resId = item.getValueInt();
+ String packageName = context.getResources().getResourcePackageName(resId);
+ String type = context.getResources().getResourceTypeName(resId);
+ String entryName = context.getResources().getResourceEntryName(resId);
+ return packageName + ".R." + type + "." + entryName;
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/generator/NotificationGenerator.java b/apps/NotificationStudio/src/com/android/notificationstudio/generator/NotificationGenerator.java
new file mode 100644
index 000000000..64b4ece28
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/generator/NotificationGenerator.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012 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.notificationstudio.generator;
+import static com.android.notificationstudio.model.EditableItem.ACTION1_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION1_TEXT;
+import static com.android.notificationstudio.model.EditableItem.ACTION2_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION2_TEXT;
+import static com.android.notificationstudio.model.EditableItem.ACTION3_ICON;
+import static com.android.notificationstudio.model.EditableItem.ACTION3_TEXT;
+import static com.android.notificationstudio.model.EditableItem.BIG_CONTENT_TITLE;
+import static com.android.notificationstudio.model.EditableItem.BIG_TEXT;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_INFO;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_TEXT;
+import static com.android.notificationstudio.model.EditableItem.CONTENT_TITLE;
+import static com.android.notificationstudio.model.EditableItem.LARGE_ICON;
+import static com.android.notificationstudio.model.EditableItem.LINES;
+import static com.android.notificationstudio.model.EditableItem.NUMBER;
+import static com.android.notificationstudio.model.EditableItem.PICTURE;
+import static com.android.notificationstudio.model.EditableItem.PROGRESS;
+import static com.android.notificationstudio.model.EditableItem.SMALL_ICON;
+import static com.android.notificationstudio.model.EditableItem.STYLE;
+import static com.android.notificationstudio.model.EditableItem.SUB_TEXT;
+import static com.android.notificationstudio.model.EditableItem.SUMMARY_TEXT;
+import static com.android.notificationstudio.model.EditableItem.USES_CHRON;
+import static com.android.notificationstudio.model.EditableItem.WHEN;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationCompat.BigPictureStyle;
+import android.support.v4.app.NotificationCompat.BigTextStyle;
+import android.support.v4.app.NotificationCompat.InboxStyle;
+
+import com.android.notificationstudio.model.EditableItemConstants;
+
+public class NotificationGenerator implements EditableItemConstants {
+
+ public static Notification build(Context context) {
+
+ PendingIntent noop = PendingIntent.getActivity(context, 0, new Intent(), 0);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
+ if (SMALL_ICON.hasValue())
+ builder.setSmallIcon(SMALL_ICON.getValueInt());
+ if (CONTENT_TITLE.hasValue())
+ builder.setContentTitle(CONTENT_TITLE.getValueString());
+ if (CONTENT_TEXT.hasValue())
+ builder.setContentText(CONTENT_TEXT.getValueString());
+ if (SUB_TEXT.hasValue())
+ builder.setSubText(SUB_TEXT.getValueString());
+ if (LARGE_ICON.hasValue())
+ builder.setLargeIcon(LARGE_ICON.getValueBitmap());
+ if (CONTENT_INFO.hasValue())
+ builder.setContentInfo(CONTENT_INFO.getValueString());
+ if (NUMBER.hasValue())
+ builder.setNumber(NUMBER.getValueInt());
+ if (WHEN.hasValue())
+ builder.setWhen(WHEN.getValueLong());
+ if (PROGRESS.hasValue() && PROGRESS.getValueBool())
+ builder.setProgress(0, 0, true);
+ if (USES_CHRON.hasValue())
+ builder.setUsesChronometer(USES_CHRON.getValueBool());
+ if (ACTION1_ICON.hasValue())
+ builder.addAction(ACTION1_ICON.getValueInt(), ACTION1_TEXT.getValueString(), noop);
+ if (ACTION2_ICON.hasValue())
+ builder.addAction(ACTION2_ICON.getValueInt(), ACTION2_TEXT.getValueString(), noop);
+ if (ACTION3_ICON.hasValue())
+ builder.addAction(ACTION3_ICON.getValueInt(), ACTION3_TEXT.getValueString(), noop);
+
+ if (STYLE.hasValue())
+ generateStyle(builder);
+
+ // for older OSes
+ builder.setContentIntent(noop);
+
+ return builder.build();
+ }
+
+ private static void generateStyle(NotificationCompat.Builder builder) {
+ Integer styleValue = STYLE.getValueInt();
+
+ if (STYLE_BIG_PICTURE.equals(styleValue)) {
+ BigPictureStyle bigPicture = new NotificationCompat.BigPictureStyle();
+ if (PICTURE.hasValue())
+ bigPicture.bigPicture(PICTURE.getValueBitmap());
+ if (BIG_CONTENT_TITLE.hasValue())
+ bigPicture.setBigContentTitle(BIG_CONTENT_TITLE.getValueString());
+ if (SUMMARY_TEXT.hasValue())
+ bigPicture.setSummaryText(SUMMARY_TEXT.getValueString());
+ builder.setStyle(bigPicture);
+ } else if (STYLE_BIG_TEXT.equals(styleValue)) {
+ BigTextStyle bigText = new NotificationCompat.BigTextStyle();
+ if (BIG_TEXT.hasValue())
+ bigText.bigText(BIG_TEXT.getValueString());
+ if (BIG_CONTENT_TITLE.hasValue())
+ bigText.setBigContentTitle(BIG_CONTENT_TITLE.getValueString());
+ if (SUMMARY_TEXT.hasValue())
+ bigText.setSummaryText(SUMMARY_TEXT.getValueString());
+ builder.setStyle(bigText);
+ } else if (STYLE_INBOX.equals(styleValue)) {
+ InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
+ if (LINES.hasValue()) {
+ for (String line : LINES.getValueString().split("\\n")) {
+ inboxStyle.addLine(line);
+ }
+ }
+ if (BIG_CONTENT_TITLE.hasValue())
+ inboxStyle.setBigContentTitle(BIG_CONTENT_TITLE.getValueString());
+ if (SUMMARY_TEXT.hasValue())
+ inboxStyle.setSummaryText(SUMMARY_TEXT.getValueString());
+ builder.setStyle(inboxStyle);
+ }
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItem.java b/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItem.java
new file mode 100644
index 000000000..54e03e019
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItem.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2012 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.notificationstudio.model;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import com.android.notificationstudio.R;
+
+public enum EditableItem implements EditableItemConstants {
+
+ PRESET(R.string.preset, TYPE_DROP_DOWN, CATEGORY_MAIN,
+ PRESET_BASIC, PRESET_EMAIL, PRESET_PHOTO, PRESET_CUSTOM),
+ SMALL_ICON(R.string.small_icon, TYPE_RESOURCE_ID, CATEGORY_MAIN,
+ SMALL_ICONS),
+ CONTENT_TITLE(R.string.content_title, TYPE_TEXT, CATEGORY_MAIN),
+ CONTENT_TEXT(R.string.content_text, TYPE_TEXT, CATEGORY_MAIN),
+ SUB_TEXT(R.string.sub_text, TYPE_TEXT, CATEGORY_MAIN),
+ LARGE_ICON(R.string.large_icon, TYPE_BITMAP, CATEGORY_MAIN),
+ CONTENT_INFO(R.string.content_info, TYPE_TEXT, CATEGORY_MAIN),
+ NUMBER(R.string.number, TYPE_INT, CATEGORY_MAIN),
+ WHEN(R.string.when, TYPE_DATETIME, CATEGORY_MAIN),
+ PROGRESS(R.string.progress, TYPE_BOOLEAN, CATEGORY_MAIN),
+ USES_CHRON(R.string.uses_chron, TYPE_BOOLEAN, CATEGORY_MAIN),
+ STYLE(R.string.style, TYPE_DROP_DOWN, CATEGORY_STYLE,
+ STYLE_NONE, STYLE_BIG_PICTURE, STYLE_BIG_TEXT, STYLE_INBOX),
+ PICTURE(R.string.picture, TYPE_BITMAP, CATEGORY_STYLE),
+ BIG_TEXT(R.string.big_text, TYPE_TEXT, CATEGORY_STYLE),
+ LINES(R.string.lines, TYPE_TEXT_LINES, CATEGORY_STYLE),
+ BIG_CONTENT_TITLE(R.string.big_content_title, TYPE_TEXT, CATEGORY_STYLE),
+ SUMMARY_TEXT(R.string.summary_text, TYPE_TEXT, CATEGORY_STYLE),
+ ACTION1_ICON(R.string.icon, TYPE_RESOURCE_ID, CATEGORY_ACTION1,
+ ACTION_ICONS),
+ ACTION1_TEXT(R.string.text, TYPE_TEXT, CATEGORY_ACTION1),
+ ACTION2_ICON(R.string.icon, TYPE_RESOURCE_ID, CATEGORY_ACTION2,
+ ACTION_ICONS),
+ ACTION2_TEXT(R.string.text, TYPE_TEXT, CATEGORY_ACTION2),
+ ACTION3_ICON(R.string.icon, TYPE_RESOURCE_ID, CATEGORY_ACTION3,
+ ACTION_ICONS),
+ ACTION3_TEXT(R.string.text, TYPE_TEXT, CATEGORY_ACTION3),
+ ;
+
+ private final int mCaptionId;
+ private final int mType;
+ private final int mCategoryId;
+
+ private Object[] mAvailableValues;
+ private Object mValue;
+ private boolean mVisible = true;
+ private Runnable mVisibilityListener;
+
+ private EditableItem(int captionId, int type, int categoryId, Object... availableValues) {
+ mCaptionId = captionId;
+ mType = type;
+ mCategoryId = categoryId;
+ mAvailableValues = availableValues;
+ }
+
+ // init
+ public static void initIfNecessary(Context context) {
+ if (PRESET.hasValue())
+ return;
+ loadBitmaps(context, LARGE_ICON, LARGE_ICONS);
+ loadBitmaps(context, PICTURE, PICTURES);
+ PRESET.setValue(PRESET_BASIC);
+ }
+
+ private static void loadBitmaps(Context context, EditableItem item, int[] bitmapResIds) {
+ Object[] largeIconBitmaps = new Object[bitmapResIds.length];
+ Resources res = context.getResources();
+ for (int i = 0; i < bitmapResIds.length; i++)
+ largeIconBitmaps[i] = BitmapFactory.decodeResource(res, bitmapResIds[i]);
+ item.setAvailableValues(largeIconBitmaps);
+ }
+
+ // visibility
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ public void setVisible(boolean visible) {
+ if (mVisible == visible)
+ return;
+ mVisible = visible;
+ if (mVisibilityListener != null)
+ mVisibilityListener.run();
+ }
+
+ public void setVisibilityListener(Runnable listener) {
+ mVisibilityListener = listener;
+ }
+
+ // value
+
+ public boolean hasValue() {
+ return mValue != null;
+ }
+
+ public void setValue(Object value) {
+ if (mValue == value)
+ return;
+ mValue = value;
+ if (this == STYLE)
+ applyStyle();
+ if (this == PRESET && !PRESET_CUSTOM.equals(value))
+ applyPreset();
+ }
+
+ private void applyStyle() {
+ PICTURE.setVisible(STYLE_BIG_PICTURE.equals(mValue));
+ BIG_TEXT.setVisible(STYLE_BIG_TEXT.equals(mValue));
+ LINES.setVisible(STYLE_INBOX.equals(mValue));
+ BIG_CONTENT_TITLE.setVisible(!STYLE_NONE.equals(mValue));
+ SUMMARY_TEXT.setVisible(!STYLE_NONE.equals(mValue));
+ }
+
+ private void applyPreset() {
+ for (EditableItem item : values())
+ if (item != PRESET)
+ item.setValue(null);
+ STYLE.setValue(STYLE_NONE);
+ if (PRESET_BASIC.equals(mValue)) {
+ SMALL_ICON.setValue(android.R.drawable.stat_notify_chat);
+ CONTENT_TITLE.setValue("Basic title");
+ CONTENT_TEXT.setValue("Basic text");
+ } else if (PRESET_EMAIL.equals(mValue)) {
+ SMALL_ICON.setValue(R.drawable.ic_notification_multiple_mail_holo_dark);
+ LARGE_ICON.setValue(LARGE_ICON.getAvailableValues()[3]);
+ CONTENT_TITLE.setValue("3 new messages");
+ CONTENT_TEXT.setValue("Alice, Bob, Chuck");
+ STYLE.setValue(STYLE_INBOX);
+ LINES.setValue("Alice: Re: Something\n" +
+ "Bob: Did you get the memo?\n" +
+ "Chuck: Limited time offer!");
+ } else if (PRESET_PHOTO.equals(mValue)) {
+ SMALL_ICON.setValue(android.R.drawable.ic_menu_camera);
+ LARGE_ICON.setValue(LARGE_ICON.getAvailableValues()[2]);
+ CONTENT_TITLE.setValue("Sunset on the rocks");
+ CONTENT_TEXT.setValue("800x534 | 405.1K");
+ SUMMARY_TEXT.setValue(CONTENT_TEXT.getValueString());
+ STYLE.setValue(STYLE_BIG_PICTURE);
+ PICTURE.setValue(PICTURE.getAvailableValues()[0]);
+ ACTION1_ICON.setValue(android.R.drawable.ic_menu_share);
+ ACTION1_TEXT.setValue("Share");
+ }
+ }
+
+ public Object getValue() {
+ return mValue;
+ }
+
+ public String getValueString() {
+ return (String) mValue;
+ }
+
+ public int getValueInt() {
+ return (Integer) mValue;
+ }
+
+ public long getValueLong() {
+ return (Long) mValue;
+ }
+
+ public boolean getValueBool() {
+ return (Boolean) mValue;
+ }
+
+ public Bitmap getValueBitmap() {
+ return (Bitmap) mValue;
+ }
+
+ // available values
+
+ public Object[] getAvailableValues() {
+ return mAvailableValues;
+ }
+
+ public Integer[] getAvailableValuesInteger() {
+ Integer[] integers = new Integer[mAvailableValues.length];
+ System.arraycopy(mAvailableValues, 0, integers, 0, integers.length);
+ return integers;
+ }
+
+ public <T> void setAvailableValues(T... values) {
+ mAvailableValues = values;
+ }
+
+ public String getCaption(Context context) {
+ return context.getString(mCaptionId);
+ }
+
+ public String getCategory(Context context) {
+ return context.getString(mCategoryId);
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+}
diff --git a/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItemConstants.java b/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItemConstants.java
new file mode 100644
index 000000000..d4defdf0c
--- /dev/null
+++ b/apps/NotificationStudio/src/com/android/notificationstudio/model/EditableItemConstants.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 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.notificationstudio.model;
+
+import com.android.notificationstudio.R;
+
+public interface EditableItemConstants {
+
+ public static final int TYPE_TEXT = 0;
+ public static final int TYPE_DROP_DOWN = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int TYPE_BITMAP = 3;
+ public static final int TYPE_INT = 4;
+ public static final int TYPE_DATETIME = 5;
+ public static final int TYPE_BOOLEAN = 6;
+ public static final int TYPE_TEXT_LINES = 7;
+
+ public static final int CATEGORY_MAIN = R.string.properties;
+ public static final int CATEGORY_STYLE = R.string.style;
+ public static final int CATEGORY_ACTION1 = R.string.action_1;
+ public static final int CATEGORY_ACTION2 = R.string.action_2;
+ public static final int CATEGORY_ACTION3 = R.string.action_3;
+
+ public static final Integer PRESET_CUSTOM = R.string.preset_custom;
+ public static final Integer PRESET_BASIC = R.string.preset_basic;
+ public static final Integer PRESET_EMAIL = R.string.preset_email;
+ public static final Integer PRESET_PHOTO = R.string.preset_photo;
+
+ public static final Integer STYLE_NONE = R.string.style_none;
+ public static final Integer STYLE_BIG_PICTURE = R.string.style_big_picture;
+ public static final Integer STYLE_BIG_TEXT = R.string.style_big_text;
+ public static final Integer STYLE_INBOX = R.string.style_inbox;
+
+ public static final Object[] SMALL_ICONS = new Object[] {
+ android.R.drawable.stat_sys_warning,
+ android.R.drawable.stat_sys_download_done,
+ android.R.drawable.stat_notify_chat,
+ android.R.drawable.stat_notify_sync,
+ android.R.drawable.stat_notify_more,
+ android.R.drawable.stat_notify_sdcard,
+ android.R.drawable.stat_sys_data_bluetooth,
+ android.R.drawable.stat_notify_voicemail,
+ android.R.drawable.stat_sys_speakerphone,
+ android.R.drawable.ic_menu_camera,
+ android.R.drawable.ic_menu_share,
+ R.drawable.ic_notification_multiple_mail_holo_dark
+ };
+
+ public static final Object[] ACTION_ICONS = SMALL_ICONS;
+
+ public static final int[] LARGE_ICONS = new int[]{
+ R.drawable.romainguy_rockaway,
+ R.drawable.android_logo,
+ R.drawable.romain,
+ R.drawable.ic_notification_multiple_mail_holo_dark
+ };
+
+ public static final int[] PICTURES = LARGE_ICONS;
+
+}
diff --git a/apps/SdkController/.classpath b/apps/SdkController/.classpath
new file mode 100755
index 000000000..a4f1e4054
--- /dev/null
+++ b/apps/SdkController/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/apps/SdkController/.project b/apps/SdkController/.project
new file mode 100755
index 000000000..a3417c5b0
--- /dev/null
+++ b/apps/SdkController/.project
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>SdkControllerApp</name>
+ <comment></comment>
+ <projects>
+ <project>SdkControllerLib</project>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/apps/SdkController/.settings/org.eclipse.jdt.core.prefs b/apps/SdkController/.settings/org.eclipse.jdt.core.prefs
new file mode 100755
index 000000000..5b174be29
--- /dev/null
+++ b/apps/SdkController/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,12 @@
+#Fri Apr 06 22:06:54 PDT 2012
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.6
diff --git a/apps/SdkController/AndroidManifest.xml b/apps/SdkController/AndroidManifest.xml
new file mode 100755
index 000000000..df7aa47d6
--- /dev/null
+++ b/apps/SdkController/AndroidManifest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tools.sdkcontroller"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="7"
+ android:targetSdkVersion="15" />
+
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name" >
+
+ <activity
+ android:name=".activities.MainActivity"
+ android:label="@string/app_name"
+ android:launchMode="singleInstance" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".activities.SensorActivity"
+ android:launchMode="singleInstance"
+ android:windowSoftInputMode="stateUnchanged" android:label="@string/sensors_activity_title"/>
+
+ <activity
+ android:name=".activities.MultiTouchActivity"
+ android:launchMode="singleInstance"
+ android:screenOrientation="portrait"
+ android:theme="@style/Theme.MultiTouch"
+ android:windowSoftInputMode="stateHidden"/>
+
+ <service
+ android:name=".service.ControllerService"
+ android:description="@string/service_description"
+ android:icon="@drawable/ic_launcher" />
+ </application>
+</manifest>
diff --git a/apps/SdkController/Implementation.txt b/apps/SdkController/Implementation.txt
new file mode 100755
index 000000000..f1ead49d9
--- /dev/null
+++ b/apps/SdkController/Implementation.txt
@@ -0,0 +1,85 @@
+Implementation Details for SdkControllerApp
+-------------------------------------------
+
+---- 2012-03-22
+App is in the namespace com.android.tools.sdkcontroller.
+
+This is an app that has a minSdkVersion of 7 (Eclair)
+and a targetSdkVersion of 15 (ICS). The target version
+means the app is forbidden under ICS from doing any network
+communication on its main thread.
+
+The overall design:
+- A background service is started by the app. It handles the connection
+ to the emulator and provides a number of "handlers". Handlers can be
+ though as being separate tasks that the user wants to achieve, for example
+ sending sensor data, sending multi-touch events, receiving screen updates,
+ sending a camera feed, etc.
+- All the handlers are started when the service starts and shutdown with it.
+ They basically stay there as long as the app is running, and its up to the
+ handler to deal with emulator connections starts/stopping. Some handlers
+ will run in the background (e.g. sending sensor data) whereas other might
+ need an activity to connect to them first.
+- The app has a number of activities which connect to existing handlers.
+
+Another way to see it is that the app handles a number of tasks which are
+composed of a background handler (that consumes data form the emulator and
+can send data to the emulator) and an optional activity for UI (that displays
+or controls the handler's state.)
+
+
+Here's a quick overview of the classes in the application:
+
+
+The main UI is in activities.MainActivity.
+There are 2 tasks activities: SensorActivity and MultiTouchActivity.
+
+These all derive from BaseBindingActivity which provides a few convenient common features
+- in onResume this will bind to the service, creating and starting it if necessary.
+- in onPause, this will unbind from the service, but does not stop it.
+
+Note however that due to the asynchronous nature of the bind operation, the activity
+must not attempt to use the service from onResume. Instead there are 2 callbacks to use:
+- onServiceConnected when the bind succeeded.
+- onServiceDisconnected as the reverse operation.
+
+When the activity is connected to the service, it can then use getServiceBinder()
+to get an interface to talk to the service.
+
+In the other direction, the activity provides a listener for the service to notify
+the application: ControllerListener createControllerListener().
+
+The activity can then access the handler:
+ handler = getServiceBinder().getHandler(HandlerType....)
+
+and then the activity wants to provide a listener to get notified by the handler:
+ handler.addUiHandler(new android.os.Handler(this));
+
+The emulator connection is separated in the "lib" subpackage:
+- EmulatorConnection abstracts a connection to the emulator.
+ - Object is first created by giving a non-null EmulatorListener.
+ - then connect(port) is called to initiate the connection.
+ - The EmulatorConnection is always created in SYNC mode.
+- EmulatorListener is a callback: the emulator connection uses it to indicate
+ when the connection is actually connected or disconnected.
+
+In the end we have the following workflow describing who controls what (-->):
+
+
+ Emulator
+ ^ ^
+ | | EmuCnxHandler
+ sendEventToEmulator| | (EmulatorListener)
+ | +-------------+
+ | |
+ handlers.BaseHandler | v
+ Activity ------------------------> Handler <---- ControllerService
+ UI <------------------------ | ^
+ android.os.Handler | |
+ | ^ | |
+ | | ControllerListener | |
+ | +--------------------------------------------------+ |
+ +-----------------------------------------------------------+
+ ControllerBinder
+
+----
diff --git a/apps/SdkController/NOTICE b/apps/SdkController/NOTICE
new file mode 100644
index 000000000..06a9081ca
--- /dev/null
+++ b/apps/SdkController/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2014, 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.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/apps/SdkController/assets/intro_help.html b/apps/SdkController/assets/intro_help.html
new file mode 100755
index 000000000..7657aa320
--- /dev/null
+++ b/apps/SdkController/assets/intro_help.html
@@ -0,0 +1,42 @@
+<html>
+<!--
+/*
+ * Copyright (C) 2012 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.
+ */
+-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <style type="text/css">
+ body { color: white; background-color: black }
+ a:link { color: #33B5E5; /*blue*/ }
+ a:visited { color: #99CC00; /*green*/ }
+ a:hover { color: #FFBB33; /*yellow*/; }
+ </style>
+</head>
+<body>
+<hr/>
+<b>SdkController</b> is used to send sensor data from an actual device to an emulator. <p/>
+To use it, do the following: <br/>
+<ol>
+<li>Connect your device to your computer via USB. Make sure to enable <i>USB Debugging</i> in <i>Settings &gt; Developer Options</i>. </li>
+<li>Start this application on your device. </li>
+<li>On the computer in a shell, run: <br/><i>adb forward tcp:1970 localabstract:android.sdk.controller</i> </li>
+<li>Finally <b>run an emulator</b> with an AVD targetting <b>API 15</b>.
+Multi-touch emulation must be explicitly enabled in emulator either by setting "Touch screen type" property to "multi-touch" in AVD Manager,
+or by starting the emulator with "-screen multi-touch" option.</li>
+</ol>
+<a href="https://sites.google.com/a/android.com/tools/recent/sensoremulation">Read more.</a>
+</body>
+</html>
diff --git a/apps/SdkController/bin/SdkControllerApp.apk b/apps/SdkController/bin/SdkControllerApp.apk
new file mode 100755
index 000000000..f8c12940f
--- /dev/null
+++ b/apps/SdkController/bin/SdkControllerApp.apk
Binary files differ
diff --git a/apps/SdkController/proguard-project.txt b/apps/SdkController/proguard-project.txt
new file mode 100755
index 000000000..f2fe1559a
--- /dev/null
+++ b/apps/SdkController/proguard-project.txt
@@ -0,0 +1,20 @@
+# To enable ProGuard in your project, edit project.properties
+# to define the proguard.config property as described in that file.
+#
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in ${sdk.dir}/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 *;
+#}
diff --git a/apps/SdkController/project.properties b/apps/SdkController/project.properties
new file mode 100755
index 000000000..9c52cb128
--- /dev/null
+++ b/apps/SdkController/project.properties
@@ -0,0 +1,14 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}\tools\proguard\proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-15
diff --git a/apps/SdkController/res/drawable-hdpi/ic_launcher.png b/apps/SdkController/res/drawable-hdpi/ic_launcher.png
new file mode 100755
index 000000000..96a442e5b
--- /dev/null
+++ b/apps/SdkController/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/apps/SdkController/res/drawable-ldpi/ic_launcher.png b/apps/SdkController/res/drawable-ldpi/ic_launcher.png
new file mode 100755
index 000000000..99238729d
--- /dev/null
+++ b/apps/SdkController/res/drawable-ldpi/ic_launcher.png
Binary files differ
diff --git a/apps/SdkController/res/drawable-mdpi/ic_launcher.png b/apps/SdkController/res/drawable-mdpi/ic_launcher.png
new file mode 100755
index 000000000..359047dfa
--- /dev/null
+++ b/apps/SdkController/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/apps/SdkController/res/drawable-xhdpi/ic_launcher.png b/apps/SdkController/res/drawable-xhdpi/ic_launcher.png
new file mode 100755
index 000000000..71c6d760f
--- /dev/null
+++ b/apps/SdkController/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/apps/SdkController/res/layout-land/sensors.xml b/apps/SdkController/res/layout-land/sensors.xml
new file mode 100755
index 000000000..1f3e2f1cd
--- /dev/null
+++ b/apps/SdkController/res/layout-land/sensors.xml
@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ >
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <TableRow
+ android:id="@+id/row1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_sample_rate"
+ android:gravity="right"
+ android:layout_marginRight="8dp"
+ />
+
+ <EditText
+ android:id="@+id/textSampleRate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ems="4"
+ android:gravity="right"
+ android:imeOptions="actionNone|flagNoExtractUi|flagNoFullscreen|"
+ android:inputType="number"
+ android:text="@string/sensors_default_sample_rate"
+ tools:ignore="HardcodedText" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_hz_per_sensor" />
+
+ </TableRow>
+
+ <TableRow
+ android:id="@+id/row2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignBaseline="@+id/row1"
+ >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_marginRight="8dp"
+ android:text="@string/sensors_actual_rate" />
+
+ <TextView
+ android:id="@+id/textActualRate"
+ android:gravity="right"
+ android:text="--"
+ tools:ignore="HardcodedText"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_hz_average" />
+
+ <!-- This 1-pixel wide invisible edit field makes sure that row1 and
+ row2 have the same height and an equal baseline. This works around
+ the fact that row2's attribute layout_alignBaseline=row1 is in fact
+ ignored. -->
+ <EditText
+ android:layout_width="1px"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNone"
+ android:focusable="false"
+ android:focusableInTouchMode="false"
+ android:visibility="invisible"
+ />
+
+ </TableRow>
+
+ </RelativeLayout>
+
+ <TableLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <TableRow
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+
+ </TableRow>
+
+ <TableRow
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+
+ </TableRow>
+
+ </TableLayout>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/sensors_top_description" />
+
+ <ScrollView
+ android:id="@+id/scrollView1"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" >
+
+ <TableLayout
+ android:id="@+id/tableLayout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:saveEnabled="false" />
+
+ </ScrollView>
+
+ <!-- Placeholder status text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textStatus"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <!-- Placeholder error text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textError"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:background="#F00F"
+ android:padding="8dp"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#FFF0" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/SdkController/res/layout/main.xml b/apps/SdkController/res/layout/main.xml
new file mode 100755
index 000000000..2e7a4bb37
--- /dev/null
+++ b/apps/SdkController/res/layout/main.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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.
+ */
+-->
+
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ >
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp" >
+
+ <ToggleButton
+ android:id="@+id/toggleService"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ />
+
+ <TextView
+ android:id="@+id/labelService"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/toggleService"
+ android:layout_alignParentLeft="true"
+ android:layout_marginTop="20dp"
+ android:text="@string/main_label_service"
+ android:textAppearance="?android:attr/textAppearanceLarge" />
+
+ <!-- Placeholder status text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBaseline="@+id/labelService"
+ android:layout_marginLeft="8dp"
+ android:layout_toRightOf="@+id/labelService"
+ android:text="[status]"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ tools:ignore="HardcodedText" />
+
+ <!-- Placeholder error text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textError"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/toggleService"
+ android:layout_marginBottom="8dp"
+ android:layout_marginTop="8dp"
+ android:background="#F00F"
+ android:gravity="center_horizontal"
+ android:padding="8dp"
+ android:text="[service errors]"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="#FFF0"
+ tools:ignore="HardcodedText" />
+
+ <TextView
+ android:id="@+id/labelButtons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/textError"
+ android:layout_marginTop="16dp"
+ android:text="@string/main_label_buttons"
+ android:textAppearance="?android:attr/textAppearanceLarge" />
+
+ <Button
+ android:id="@+id/btnOpenMultitouch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/labelButtons"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="16dp"
+ android:text="@string/main_btn_open_multitouch" />
+
+ <Button
+ android:id="@+id/btnOpenSensors"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/btnOpenMultitouch"
+ android:layout_centerHorizontal="true"
+ android:layout_marginTop="16dp"
+ android:text="@string/main_btn_open_sensors" />
+
+ <WebView
+ android:id="@+id/webIntro"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/btnOpenSensors"
+ android:layout_marginTop="16dp"
+ android:background="@null"
+ />
+
+ </RelativeLayout>
+</ScrollView>
diff --git a/apps/SdkController/res/layout/multitouch.xml b/apps/SdkController/res/layout/multitouch.xml
new file mode 100755
index 000000000..0aec0fbd0
--- /dev/null
+++ b/apps/SdkController/res/layout/multitouch.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" >
+
+ <com.android.tools.sdkcontroller.views.MultiTouchView
+ android:id="@+id/imageView"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+
+ <!-- Placeholder status text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textStatus"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <!-- Placeholder error text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textError"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@+id/textStatus"
+ android:gravity="center_horizontal"
+ android:background="#F00F"
+ android:padding="8dp"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#FFF0" />
+
+</RelativeLayout>
diff --git a/apps/SdkController/res/layout/sensor_row.xml b/apps/SdkController/res/layout/sensor_row.xml
new file mode 100755
index 000000000..16ffd4272
--- /dev/null
+++ b/apps/SdkController/res/layout/sensor_row.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2011 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.
+ */
+-->
+
+<!-- One row per sensor added to the TableLayout from layout/sensors.xml -->
+<TableRow xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" >
+
+ <CheckBox
+ android:id="@+id/row_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="10dp"
+ android:saveEnabled="false"
+ android:text="Some CheckBox"
+ tools:ignore="HardcodedText" />
+
+ <TextView
+ android:id="@+id/row_textview"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+</TableRow>
diff --git a/apps/SdkController/res/layout/sensors.xml b/apps/SdkController/res/layout/sensors.xml
new file mode 100755
index 000000000..afdab02a3
--- /dev/null
+++ b/apps/SdkController/res/layout/sensors.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical"
+ >
+
+ <TableLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ >
+
+ <TableRow
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_sample_rate"
+ android:gravity="right"
+ android:layout_marginRight="8dp"
+ />
+ <EditText
+ android:id="@+id/textSampleRate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ems="4"
+ android:gravity="right"
+ android:imeOptions="actionNone|flagNoExtractUi|flagNoFullscreen|"
+ android:inputType="number"
+ android:text="@string/sensors_default_sample_rate" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_hz_per_sensor" />
+
+ </TableRow>
+
+ <TableRow
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_marginRight="8dp"
+ android:text="@string/sensors_actual_rate" />
+
+ <TextView
+ android:id="@+id/textActualRate"
+ android:gravity="right"
+ android:text="--"
+ tools:ignore="HardcodedText"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/sensors_hz_average" />
+
+ </TableRow>
+
+ </TableLayout>
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/sensors_top_description" />
+
+ <ScrollView
+ android:id="@+id/scrollView1"
+ android:layout_width="fill_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1" >
+
+ <TableLayout
+ android:id="@+id/tableLayout"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:saveEnabled="false" />
+
+ </ScrollView>
+
+ <!-- Placeholder status text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textStatus"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+ <!-- Placeholder error text. Becomes visibility=gone when empty. -->
+ <TextView
+ android:id="@+id/textError"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:background="#F00F"
+ android:padding="8dp"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="#FFF0" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/apps/SdkController/res/values-v11/styles_v11.xml b/apps/SdkController/res/values-v11/styles_v11.xml
new file mode 100755
index 000000000..3d3860e87
--- /dev/null
+++ b/apps/SdkController/res/values-v11/styles_v11.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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>
+
+ <style name="Theme.MultiTouch" parent="android:Theme.Holo.NoActionBar.Fullscreen">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ </style>
+
+</resources>
diff --git a/apps/SdkController/res/values/strings.xml b/apps/SdkController/res/values/strings.xml
new file mode 100755
index 000000000..e4e1dbb17
--- /dev/null
+++ b/apps/SdkController/res/values/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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>
+
+ <!-- Strings for manifest. -->
+ <string name="app_name">SDK Controller</string>
+ <string name="service_description">Background service for SDK Controller</string>
+
+ <!-- Strings for service. -->
+ <string name="service_notif_title">SDK Controller is running</string>
+
+ <!-- Strings for layout/main -->
+ <string name="main_label_service">Service:</string>
+ <string name="main_label_buttons">What you can do:</string>
+ <string name="main_btn_open_multitouch">Control Multi-touch</string>
+ <string name="main_btn_open_sensors">Control Sensors</string>
+ <string name="main_service_status_connected">Emulator Connected</string>
+ <string name="main_service_status_disconnected">Emulator Connected</string>
+
+ <!-- Strings for layout/sensors -->
+ <string name="sensors_activity_title">SDK Controller &gt; Sensors</string>
+ <string name="sensors_top_description">Available Sensors:</string>
+ <string name="sensors_sample_rate">Sample Rate</string>
+ <string name="sensors_hz_per_sensor">Hz per sensor</string>
+ <string name="sensors_actual_rate">Actual</string>
+ <string name="sensors_hz_average">Hz average</string>
+ <!-- Default sample rate for SensorsActivity UI.
+ Should match the default for SensorsHandler.mUpdateTargetMs. -->
+ <string name="sensors_default_sample_rate">20</string>
+
+</resources>
diff --git a/apps/SdkController/res/values/styles.xml b/apps/SdkController/res/values/styles.xml
new file mode 100755
index 000000000..67c7278fa
--- /dev/null
+++ b/apps/SdkController/res/values/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2012 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>
+
+ <style name="Theme.MultiTouch" parent="android:Theme.NoTitleBar.Fullscreen">
+ <item name="android:windowBackground">@android:color/transparent</item>
+ </style>
+
+</resources>
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java
new file mode 100755
index 000000000..ab5306ddc
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/BaseBindingActivity.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.activities;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.service.ControllerService;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener;
+
+/**
+ * Base activity class that knows how to bind and unbind from the
+ * {@link ControllerService}.
+ */
+public abstract class BaseBindingActivity extends Activity {
+
+ public static String TAG = BaseBindingActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+ private ServiceConnection mServiceConnection;
+ private ControllerBinder mServiceBinder;
+
+ /**
+ * Returns the binder. Activities can use that to query the controller service.
+ * @return An existing {@link ControllerBinder}.
+ * The binder is only valid between calls {@link #onServiceConnected()} and
+ * {@link #onServiceDisconnected()}. Returns null when not valid.
+ */
+ public ControllerBinder getServiceBinder() {
+ return mServiceBinder;
+ }
+
+ /**
+ * Called when the activity resumes.
+ * This automatically binds to the service, starting it as needed.
+ * <p/>
+ * Since on resume we automatically bind to the service, the {@link ServiceConnection}
+ * will is restored and {@link #onServiceConnected()} is called as necessary.
+ * Derived classes that need to initialize anything that is related to the service
+ * (e.g. getting their handler) should thus do so in {@link #onServiceConnected()} and
+ * <em>not</em> in {@link #onResume()} -- since binding to the service is asynchronous
+ * there is <em>no</em> guarantee that {@link #getServiceBinder()} returns non-null
+ * when this call finishes.
+ */
+ @Override
+ protected void onResume() {
+ super.onResume();
+ bindToService();
+ }
+
+ /**
+ * Called when the activity is paused.
+ * This automatically unbinds from the service but does not stop it.
+ */
+ @Override
+ protected void onPause() {
+ super.onPause();
+ unbindFromService();
+ }
+
+ // ----------
+
+ /**
+ * Called when binding to the service to get the activity's {@link ControllerListener}.
+ * @return A new non-null {@link ControllerListener}.
+ */
+ protected abstract ControllerListener createControllerListener();
+
+ /**
+ * Called by the service once the activity is connected (bound) to it.
+ * <p/>
+ * When this is called, {@link #getServiceBinder()} returns a non-null binder that
+ * can be used by the activity to control the service.
+ */
+ protected abstract void onServiceConnected();
+
+ /**
+ * Called by the service when it is forcibly disconnected OR when we know
+ * we're unbinding the service.
+ * <p/>
+ * When this is called, {@link #getServiceBinder()} returns a null binder and
+ * the activity should stop using that binder and remove any reference to it.
+ */
+ protected abstract void onServiceDisconnected();
+
+ /**
+ * Starts the service and binds to it.
+ */
+ protected void bindToService() {
+ if (mServiceConnection == null) {
+ final ControllerListener listener = createControllerListener();
+
+ mServiceConnection = new ServiceConnection() {
+ /**
+ * Called when the service is connected.
+ * Allows us to retrieve the binder to talk to the service.
+ */
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) Log.d(TAG, "Activity connected to service");
+ mServiceBinder = (ControllerBinder) service;
+ mServiceBinder.addControllerListener(listener);
+ BaseBindingActivity.this.onServiceConnected();
+ }
+
+ /**
+ * Called when the service got disconnected, e.g. because it crashed.
+ * This is <em>not</em> called when we unbind from the service.
+ */
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) Log.d(TAG, "Activity disconnected from service");
+ mServiceBinder = null;
+ BaseBindingActivity.this.onServiceDisconnected();
+ }
+ };
+ }
+
+ // Start service so that it doesn't stop when we unbind
+ if (DEBUG) Log.d(TAG, "start requested & bind service");
+ Intent service = new Intent(this, ControllerService.class);
+ startService(service);
+ bindService(service,
+ mServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ /**
+ * Unbinds from the service but does not actually stop the service.
+ * This lets us have it run in the background even if this isn't the active activity.
+ */
+ protected void unbindFromService() {
+ if (mServiceConnection != null) {
+ if (DEBUG) Log.d(TAG, "unbind service");
+ mServiceConnection.onServiceDisconnected(null /*name*/);
+ unbindService(mServiceConnection);
+ mServiceConnection = null;
+ }
+ }
+} \ No newline at end of file
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java
new file mode 100755
index 000000000..47692454c
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MainActivity.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.service.ControllerService;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener;
+
+/**
+ * Main activity. It's the entry point for the application.
+ * It allows the user to start/stop the service and see it's current state and errors.
+ * It also has buttons to start either the sensor control activity or the multitouch activity.
+ */
+public class MainActivity extends BaseBindingActivity {
+
+ @SuppressWarnings("hiding")
+ public static String TAG = MainActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+ private Button mBtnOpenMultitouch;
+ private Button mBtnOpenSensors;
+ private ToggleButton mBtnToggleService;
+ private TextView mTextError;
+ private TextView mTextStatus;
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+
+ mTextError = (TextView) findViewById(R.id.textError);
+ mTextStatus = (TextView) findViewById(R.id.textStatus);
+
+ WebView wv = (WebView) findViewById(R.id.webIntro);
+ wv.loadUrl("file:///android_asset/intro_help.html");
+
+ setupButtons();
+ }
+
+ @Override
+ protected void onResume() {
+ // BaseBindingActivity.onResume will bind to the service.
+ super.onResume();
+ updateError();
+ }
+
+ @Override
+ protected void onPause() {
+ // BaseBindingActivity.onResume will unbind from (but not stop) the service.
+ super.onPause();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (DEBUG) Log.d(TAG, "onBackPressed");
+ // If back is pressed, we stop the service automatically.
+ // It seems more intuitive that way.
+ stopService();
+ super.onBackPressed();
+ }
+
+ // ----------
+
+ @Override
+ protected void onServiceConnected() {
+ updateButtons();
+ }
+
+ @Override
+ protected void onServiceDisconnected() {
+ updateButtons();
+ }
+
+ @Override
+ protected ControllerListener createControllerListener() {
+ return new MainControllerListener();
+ }
+
+ // ----------
+
+ private void setupButtons() {
+ mBtnOpenMultitouch = (Button) findViewById(R.id.btnOpenMultitouch);
+ mBtnOpenSensors = (Button) findViewById(R.id.btnOpenSensors);
+
+ mBtnOpenMultitouch.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Open the multi-touch activity.
+ Intent i = new Intent(MainActivity.this, MultiTouchActivity.class);
+ startActivity(i);
+ }
+ });
+
+ mBtnOpenSensors.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Open the sensor activity.
+ Intent i = new Intent(MainActivity.this, SensorActivity.class);
+ startActivity(i);
+ }
+ });
+
+ mBtnToggleService = (ToggleButton) findViewById(R.id.toggleService);
+
+ // set initial state
+ updateButtons();
+
+ mBtnToggleService.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ bindToService();
+ updateButtons();
+ } else {
+ stopService();
+ updateButtons();
+ }
+ }
+ });
+
+ }
+
+ private void updateButtons() {
+ boolean running = ControllerService.isServiceIsRunning();
+ mBtnOpenMultitouch.setEnabled(running);
+ mBtnOpenSensors.setEnabled(running);
+ mBtnToggleService.setChecked(running);
+ }
+
+ /**
+ * Unbind and then actually stops the service.
+ */
+ private void stopService() {
+ Intent service = new Intent(this, ControllerService.class);
+ unbindFromService();
+ if (DEBUG) Log.d(TAG, "stop service requested");
+ stopService(service);
+ }
+
+ private class MainControllerListener implements ControllerListener {
+ @Override
+ public void onErrorChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateError();
+ }
+ });
+ }
+
+ @Override
+ public void onStatusChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateStatus();
+ }
+ });
+ }
+ }
+
+ private void updateError() {
+ ControllerBinder binder = getServiceBinder();
+ String error = binder == null ? "" : binder.getServiceError();
+ if (error == null) {
+ error = "";
+ }
+
+ mTextError.setVisibility(error.length() == 0 ? View.GONE : View.VISIBLE);
+ mTextError.setText(error);
+ }
+
+ private void updateStatus() {
+ ControllerBinder binder = getServiceBinder();
+ boolean connected = binder == null ? false : binder.isEmuConnected();
+ mTextStatus.setText(
+ getText(connected ? R.string.main_service_status_connected
+ : R.string.main_service_status_disconnected));
+
+ }
+} \ No newline at end of file
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java
new file mode 100755
index 000000000..faba8828f
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/MultiTouchActivity.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.activities;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.widget.TextView;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.handlers.MultiTouchChannel;
+import com.android.tools.sdkcontroller.lib.Channel;
+import com.android.tools.sdkcontroller.lib.ProtocolConstants;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener;
+import com.android.tools.sdkcontroller.utils.ApiHelper;
+import com.android.tools.sdkcontroller.views.MultiTouchView;
+
+/**
+ * Activity that controls and displays the {@link MultiTouchChannel}.
+ */
+public class MultiTouchActivity extends BaseBindingActivity
+ implements android.os.Handler.Callback {
+
+ @SuppressWarnings("hiding")
+ private static String TAG = MultiTouchActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+
+ private volatile MultiTouchChannel mHandler;
+
+ private TextView mTextError;
+ private TextView mTextStatus;
+ private MultiTouchView mImageView;
+ /** Width of the emulator's display. */
+ private int mEmulatorWidth = 0;
+ /** Height of the emulator's display. */
+ private int mEmulatorHeight = 0;
+ /** Bitmap storage. */
+ private int[] mColors;
+
+ private final TouchListener mTouchListener = new TouchListener();
+ private final android.os.Handler mUiHandler = new android.os.Handler(this);
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.multitouch);
+ mImageView = (MultiTouchView) findViewById(R.id.imageView);
+ mTextError = (TextView) findViewById(R.id.textError);
+ mTextStatus = (TextView) findViewById(R.id.textStatus);
+ updateStatus("Waiting for connection");
+
+ ApiHelper ah = ApiHelper.get();
+ ah.View_setSystemUiVisibility(mImageView, View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ @Override
+ protected void onResume() {
+ if (DEBUG) Log.d(TAG, "onResume");
+ // BaseBindingActivity.onResume will bind to the service.
+ // Note: any initialization related to the service or the handler should
+ // go in onServiceConnected() since in this call the service may not be
+ // bound yet.
+ super.onResume();
+ updateError();
+ }
+
+ @Override
+ protected void onPause() {
+ if (DEBUG) Log.d(TAG, "onPause");
+ // BaseBindingActivity.onResume will unbind from (but not stop) the service.
+ super.onPause();
+ mImageView.setEnabled(false);
+ updateStatus("Paused");
+ }
+
+ // ----------
+
+ @Override
+ protected void onServiceConnected() {
+ if (DEBUG) Log.d(TAG, "onServiceConnected");
+ mHandler = (MultiTouchChannel) getServiceBinder().getChannel(Channel.MULTITOUCH_CHANNEL);
+ if (mHandler != null) {
+ mHandler.setViewSize(mImageView.getWidth(), mImageView.getHeight());
+ mHandler.addUiHandler(mUiHandler);
+ }
+ }
+
+ @Override
+ protected void onServiceDisconnected() {
+ if (DEBUG) Log.d(TAG, "onServiceDisconnected");
+ if (mHandler != null) {
+ mHandler.removeUiHandler(mUiHandler);
+ mHandler = null;
+ }
+ }
+
+ @Override
+ protected ControllerListener createControllerListener() {
+ return new MultiTouchControllerListener();
+ }
+
+ // ----------
+
+ private class MultiTouchControllerListener implements ControllerListener {
+ @Override
+ public void onErrorChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateError();
+ }
+ });
+ }
+
+ @Override
+ public void onStatusChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ControllerBinder binder = getServiceBinder();
+ if (binder != null) {
+ boolean connected = binder.isEmuConnected();
+ mImageView.setEnabled(connected);
+ updateStatus(connected ? "Emulator connected" : "Emulator disconnected");
+ }
+ }
+ });
+ }
+ }
+
+ // ----------
+
+ /**
+ * Implements OnTouchListener interface that receives touch screen events,
+ * and reports them to the emulator application.
+ */
+ class TouchListener implements OnTouchListener {
+ /**
+ * Touch screen event handler.
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ ByteBuffer bb = null;
+ final int action = event.getAction();
+ final int action_code = action & MotionEvent.ACTION_MASK;
+ final int action_pid_index = action >> MotionEvent.ACTION_POINTER_ID_SHIFT;
+ int msg_type = 0;
+ MultiTouchChannel h = mHandler;
+
+ // Build message for the emulator.
+ switch (action_code) {
+ case MotionEvent.ACTION_MOVE:
+ if (h != null) {
+ bb = ByteBuffer.allocate(
+ event.getPointerCount() * ProtocolConstants.MT_EVENT_ENTRY_SIZE);
+ bb.order(h.getEndian());
+ for (int n = 0; n < event.getPointerCount(); n++) {
+ mImageView.constructEventMessage(bb, event, n);
+ }
+ msg_type = ProtocolConstants.MT_MOVE;
+ }
+ break;
+ case MotionEvent.ACTION_DOWN:
+ if (h != null) {
+ bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE);
+ bb.order(h.getEndian());
+ mImageView.constructEventMessage(bb, event, action_pid_index);
+ msg_type = ProtocolConstants.MT_FISRT_DOWN;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (h != null) {
+ bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE);
+ bb.order(h.getEndian());
+ bb.putInt(event.getPointerId(action_pid_index));
+ msg_type = ProtocolConstants.MT_LAST_UP;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ if (h != null) {
+ bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE);
+ bb.order(h.getEndian());
+ mImageView.constructEventMessage(bb, event, action_pid_index);
+ msg_type = ProtocolConstants.MT_POINTER_DOWN;
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ if (h != null) {
+ bb = ByteBuffer.allocate(ProtocolConstants.MT_EVENT_ENTRY_SIZE);
+ bb.order(h.getEndian());
+ bb.putInt(event.getPointerId(action_pid_index));
+ msg_type = ProtocolConstants.MT_POINTER_UP;
+ }
+ break;
+ default:
+ Log.w(TAG, "Unknown action type: " + action_code);
+ return true;
+ }
+
+ if (DEBUG && bb != null) Log.d(TAG, bb.toString());
+
+ if (h != null && bb != null) {
+ h.postMessage(msg_type, bb);
+ }
+ return true;
+ }
+ } // TouchListener
+
+ /** Implementation of Handler.Callback */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MultiTouchChannel.EVENT_MT_START:
+ MultiTouchChannel h = mHandler;
+ if (h != null) {
+ mImageView.setEnabled(true);
+ mImageView.setOnTouchListener(mTouchListener);
+ }
+ break;
+ case MultiTouchChannel.EVENT_MT_STOP:
+ mImageView.setOnTouchListener(null);
+ break;
+ case MultiTouchChannel.EVENT_FRAME_BUFFER:
+ onFrameBuffer(((ByteBuffer) msg.obj).array());
+ mHandler.postMessage(ProtocolConstants.MT_FB_HANDLED, (byte[]) null);
+ break;
+ }
+ return true; // we consumed this message
+ }
+
+ /**
+ * Called when a BLOB query is received from the emulator.
+ * <p/>
+ * This query is used to deliver framebuffer updates in the emulator. The
+ * blob contains an update header, followed by the bitmap containing updated
+ * rectangle. The header is defined as MTFrameHeader structure in
+ * external/qemu/android/multitouch-port.h
+ * <p/>
+ * NOTE: This method is called from the I/O loop, so all communication with
+ * the emulator will be "on hold" until this method returns.
+ *
+ * TODO ===> CHECK that we can consume that array from a different thread than the producer's.
+ * E.g. does the produce reuse the same array or does it generate a new one each time?
+ *
+ * @param array contains BLOB data for the query.
+ */
+ private void onFrameBuffer(byte[] array) {
+ final ByteBuffer bb = ByteBuffer.wrap(array);
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+
+ // Read frame header.
+ final int header_size = bb.getInt();
+ final int disp_width = bb.getInt();
+ final int disp_height = bb.getInt();
+ final int x = bb.getInt();
+ final int y = bb.getInt();
+ final int w = bb.getInt();
+ final int h = bb.getInt();
+ final int bpl = bb.getInt();
+ final int bpp = bb.getInt();
+ final int format = bb.getInt();
+
+ // Update application display.
+ updateDisplay(disp_width, disp_height);
+
+ if (format == ProtocolConstants.MT_FRAME_JPEG) {
+ /*
+ * Framebuffer is in JPEG format.
+ */
+
+ final ByteArrayInputStream jpg = new ByteArrayInputStream(bb.array());
+ // Advance input stream to JPEG image.
+ jpg.skip(header_size);
+ // Draw the image.
+ mImageView.drawJpeg(x, y, w, h, jpg);
+ } else {
+ /*
+ * Framebuffer is in a raw RGB format.
+ */
+
+ final int pixel_num = h * w;
+ // Advance stream to the beginning of framebuffer data.
+ bb.position(header_size);
+
+ // Make sure that mColors is large enough to contain the
+ // update bitmap.
+ if (mColors == null || mColors.length < pixel_num) {
+ mColors = new int[pixel_num];
+ }
+
+ // Convert the blob bitmap into bitmap that we will display.
+ if (format == ProtocolConstants.MT_FRAME_RGB565) {
+ for (int n = 0; n < pixel_num; n++) {
+ // Blob bitmap is in RGB565 format.
+ final int color = bb.getShort();
+ final int r = ((color & 0xf800) >> 8) | ((color & 0xf800) >> 14);
+ final int g = ((color & 0x7e0) >> 3) | ((color & 0x7e0) >> 9);
+ final int b = ((color & 0x1f) << 3) | ((color & 0x1f) >> 2);
+ mColors[n] = Color.rgb(r, g, b);
+ }
+ } else if (format == ProtocolConstants.MT_FRAME_RGB888) {
+ for (int n = 0; n < pixel_num; n++) {
+ // Blob bitmap is in RGB565 format.
+ final int r = bb.getChar();
+ final int g = bb.getChar();
+ final int b = bb.getChar();
+ mColors[n] = Color.rgb(r, g, b);
+ }
+ } else {
+ Log.w(TAG, "Invalid framebuffer format: " + format);
+ return;
+ }
+ mImageView.drawBitmap(x, y, w, h, mColors);
+ }
+ }
+
+ /**
+ * Updates application's screen accordingly to the emulator screen.
+ *
+ * @param e_width Width of the emulator screen.
+ * @param e_height Height of the emulator screen.
+ */
+ private void updateDisplay(int e_width, int e_height) {
+ if (e_width != mEmulatorWidth || e_height != mEmulatorHeight) {
+ mEmulatorWidth = e_width;
+ mEmulatorHeight = e_height;
+
+ boolean rotateDisplay = false;
+ int w = mImageView.getWidth();
+ int h = mImageView.getHeight();
+ if (w > h != e_width > e_height) {
+ rotateDisplay = true;
+ int tmp = w;
+ w = h;
+ h = tmp;
+ }
+
+ float dx = (float) w / (float) e_width;
+ float dy = (float) h / (float) e_height;
+ mImageView.setDxDy(dx, dy, rotateDisplay);
+ if (DEBUG) Log.d(TAG, "Dispay updated: " + e_width + " x " + e_height +
+ " -> " + w + " x " + h + " ratio: " +
+ dx + " x " + dy);
+ }
+ }
+
+ // ----------
+
+ private void updateStatus(String status) {
+ mTextStatus.setVisibility(status == null ? View.GONE : View.VISIBLE);
+ if (status != null) mTextStatus.setText(status);
+ }
+
+ private void updateError() {
+ ControllerBinder binder = getServiceBinder();
+ String error = binder == null ? "" : binder.getServiceError();
+ if (error == null) {
+ error = "";
+ }
+
+ mTextError.setVisibility(error.length() == 0 ? View.GONE : View.VISIBLE);
+ mTextError.setText(error);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java
new file mode 100755
index 000000000..61c308152
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/activities/SensorActivity.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.activities;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import android.os.Bundle;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnKeyListener;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.handlers.SensorChannel;
+import com.android.tools.sdkcontroller.handlers.SensorChannel.MonitoredSensor;
+import com.android.tools.sdkcontroller.lib.Channel;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerBinder;
+import com.android.tools.sdkcontroller.service.ControllerService.ControllerListener;
+
+/**
+ * Activity that displays and controls the sensors from {@link SensorChannel}.
+ * For each sensor it displays a checkbox that is enabled if the sensor is supported
+ * by the emulator. The user can select whether the sensor is active. It also displays
+ * data from the sensor when available.
+ */
+public class SensorActivity extends BaseBindingActivity
+ implements android.os.Handler.Callback {
+
+ @SuppressWarnings("hiding")
+ public static String TAG = SensorActivity.class.getSimpleName();
+ private static boolean DEBUG = true;
+
+ private static final int MSG_UPDATE_ACTUAL_HZ = 0x31415;
+
+ private TableLayout mTableLayout;
+ private TextView mTextError;
+ private TextView mTextStatus;
+ private TextView mTextTargetHz;
+ private TextView mTextActualHz;
+ private SensorChannel mSensorHandler;
+
+ private final Map<MonitoredSensor, DisplayInfo> mDisplayedSensors =
+ new HashMap<SensorChannel.MonitoredSensor, SensorActivity.DisplayInfo>();
+ private final android.os.Handler mUiHandler = new android.os.Handler(this);
+ private int mTargetSampleRate;
+ private long mLastActualUpdateMs;
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.sensors);
+ mTableLayout = (TableLayout) findViewById(R.id.tableLayout);
+ mTextError = (TextView) findViewById(R.id.textError);
+ mTextStatus = (TextView) findViewById(R.id.textStatus);
+ mTextTargetHz = (TextView) findViewById(R.id.textSampleRate);
+ mTextActualHz = (TextView) findViewById(R.id.textActualRate);
+ updateStatus("Waiting for connection");
+
+ mTextTargetHz.setOnKeyListener(new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ updateSampleRate();
+ return false;
+ }
+ });
+ mTextTargetHz.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ updateSampleRate();
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ if (DEBUG) Log.d(TAG, "onResume");
+ // BaseBindingActivity.onResume will bind to the service.
+ super.onResume();
+ updateError();
+ }
+
+ @Override
+ protected void onPause() {
+ if (DEBUG) Log.d(TAG, "onPause");
+ // BaseBindingActivity.onResume will unbind from (but not stop) the service.
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (DEBUG) Log.d(TAG, "onDestroy");
+ super.onDestroy();
+ removeSensorUi();
+ }
+
+ // ----------
+
+ @Override
+ protected void onServiceConnected() {
+ if (DEBUG) Log.d(TAG, "onServiceConnected");
+ createSensorUi();
+ }
+
+ @Override
+ protected void onServiceDisconnected() {
+ if (DEBUG) Log.d(TAG, "onServiceDisconnected");
+ removeSensorUi();
+ }
+
+ @Override
+ protected ControllerListener createControllerListener() {
+ return new SensorsControllerListener();
+ }
+
+ // ----------
+
+ private class SensorsControllerListener implements ControllerListener {
+ @Override
+ public void onErrorChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ updateError();
+ }
+ });
+ }
+
+ @Override
+ public void onStatusChanged() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ControllerBinder binder = getServiceBinder();
+ if (binder != null) {
+ boolean connected = binder.isEmuConnected();
+ mTableLayout.setEnabled(connected);
+ updateStatus(connected ? "Emulated connected" : "Emulator disconnected");
+ }
+ }
+ });
+ }
+ }
+
+ private void createSensorUi() {
+ final LayoutInflater inflater = getLayoutInflater();
+
+ if (!mDisplayedSensors.isEmpty()) {
+ removeSensorUi();
+ }
+
+ mSensorHandler = (SensorChannel) getServiceBinder().getChannel(Channel.SENSOR_CHANNEL);
+ if (mSensorHandler != null) {
+ mSensorHandler.addUiHandler(mUiHandler);
+ mUiHandler.sendEmptyMessage(MSG_UPDATE_ACTUAL_HZ);
+
+ assert mDisplayedSensors.isEmpty();
+ List<MonitoredSensor> sensors = mSensorHandler.getSensors();
+ for (MonitoredSensor sensor : sensors) {
+ final TableRow row = (TableRow) inflater.inflate(R.layout.sensor_row,
+ mTableLayout,
+ false);
+ mTableLayout.addView(row);
+ mDisplayedSensors.put(sensor, new DisplayInfo(sensor, row));
+ }
+ }
+ }
+
+ private void removeSensorUi() {
+ if (mSensorHandler != null) {
+ mSensorHandler.removeUiHandler(mUiHandler);
+ mSensorHandler = null;
+ }
+ mTableLayout.removeAllViews();
+ for (DisplayInfo info : mDisplayedSensors.values()) {
+ info.release();
+ }
+ mDisplayedSensors.clear();
+ }
+
+ private class DisplayInfo implements CompoundButton.OnCheckedChangeListener {
+ private MonitoredSensor mSensor;
+ private CheckBox mChk;
+ private TextView mVal;
+
+ public DisplayInfo(MonitoredSensor sensor, TableRow row) {
+ mSensor = sensor;
+
+ // Initialize displayed checkbox for this sensor, and register
+ // checked state listener for it.
+ mChk = (CheckBox) row.findViewById(R.id.row_checkbox);
+ mChk.setText(sensor.getUiName());
+ mChk.setEnabled(sensor.isEnabledByEmulator());
+ mChk.setChecked(sensor.isEnabledByUser());
+ mChk.setOnCheckedChangeListener(this);
+
+ // Initialize displayed text box for this sensor.
+ mVal = (TextView) row.findViewById(R.id.row_textview);
+ mVal.setText(sensor.getValue());
+ }
+
+ /**
+ * Handles checked state change for the associated CheckBox. If check
+ * box is checked we will register sensor change listener. If it is
+ * unchecked, we will unregister sensor change listener.
+ */
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (mSensor != null) {
+ mSensor.onCheckedChanged(isChecked);
+ }
+ }
+
+ public void release() {
+ mChk = null;
+ mVal = null;
+ mSensor = null;
+
+ }
+
+ public void updateState() {
+ if (mChk != null && mSensor != null) {
+ mChk.setEnabled(mSensor.isEnabledByEmulator());
+ mChk.setChecked(mSensor.isEnabledByUser());
+ }
+ }
+
+ public void updateValue() {
+ if (mVal != null && mSensor != null) {
+ mVal.setText(mSensor.getValue());
+ }
+ }
+ }
+
+ /** Implementation of Handler.Callback */
+ @Override
+ public boolean handleMessage(Message msg) {
+ DisplayInfo info = null;
+ switch (msg.what) {
+ case SensorChannel.SENSOR_STATE_CHANGED:
+ info = mDisplayedSensors.get(msg.obj);
+ if (info != null) {
+ info.updateState();
+ }
+ break;
+ case SensorChannel.SENSOR_DISPLAY_MODIFIED:
+ info = mDisplayedSensors.get(msg.obj);
+ if (info != null) {
+ info.updateValue();
+ }
+ if (mSensorHandler != null) {
+ updateStatus(Integer.toString(mSensorHandler.getMsgSentCount()) + " events sent");
+
+ // Update the "actual rate" field if the value has changed
+ long ms = mSensorHandler.getActualUpdateMs();
+ if (ms != mLastActualUpdateMs) {
+ mLastActualUpdateMs = ms;
+ String hz = mLastActualUpdateMs <= 0 ? "--" :
+ Integer.toString((int) Math.ceil(1000. / ms));
+ mTextActualHz.setText(hz);
+ }
+ }
+ break;
+ case MSG_UPDATE_ACTUAL_HZ:
+ if (mSensorHandler != null) {
+ // Update the "actual rate" field if the value has changed
+ long ms = mSensorHandler.getActualUpdateMs();
+ if (ms != mLastActualUpdateMs) {
+ mLastActualUpdateMs = ms;
+ String hz = mLastActualUpdateMs <= 0 ? "--" :
+ Integer.toString((int) Math.ceil(1000. / ms));
+ mTextActualHz.setText(hz);
+ }
+ mUiHandler.sendEmptyMessageDelayed(MSG_UPDATE_ACTUAL_HZ, 1000 /*1s*/);
+ }
+ }
+ return true; // we consumed this message
+ }
+
+ private void updateStatus(String status) {
+ mTextStatus.setVisibility(status == null ? View.GONE : View.VISIBLE);
+ if (status != null) mTextStatus.setText(status);
+ }
+
+ private void updateError() {
+ ControllerBinder binder = getServiceBinder();
+ String error = binder == null ? "" : binder.getServiceError();
+ if (error == null) {
+ error = "";
+ }
+
+ mTextError.setVisibility(error.length() == 0 ? View.GONE : View.VISIBLE);
+ mTextError.setText(error);
+ }
+
+ private void updateSampleRate() {
+ String str = mTextTargetHz.getText().toString();
+ try {
+ int hz = Integer.parseInt(str.trim());
+
+ // Cap the value. 50 Hz is a reasonable max value for the emulator.
+ if (hz <= 0 || hz > 50) {
+ hz = 50;
+ }
+
+ if (hz != mTargetSampleRate) {
+ mTargetSampleRate = hz;
+ if (mSensorHandler != null) {
+ mSensorHandler.setUpdateTargetMs(hz <= 0 ? 0 : (int)(1000.0f / hz));
+ }
+ }
+ } catch (Exception ignore) {}
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchChannel.java b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchChannel.java
new file mode 100755
index 000000000..ad00e921e
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/MultiTouchChannel.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.handlers;
+
+import android.graphics.Point;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.lib.Channel;
+import com.android.tools.sdkcontroller.lib.ProtocolConstants;
+import com.android.tools.sdkcontroller.service.ControllerService;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Implements multi-touch emulation.
+ */
+public class MultiTouchChannel extends Channel {
+
+ @SuppressWarnings("hiding")
+ private static final String TAG = MultiTouchChannel.class.getSimpleName();
+ /**
+ * A new frame buffer has been received from the emulator.
+ * Parameter {@code obj} is a {@code byte[] array} containing the screen data.
+ */
+ public static final int EVENT_FRAME_BUFFER = 1;
+ /**
+ * A multi-touch "start" command has been received from the emulator.
+ * Parameter {@code obj} is the string parameter from the start command.
+ */
+ public static final int EVENT_MT_START = 2;
+ /**
+ * A multi-touch "stop" command has been received from the emulator. There
+ * is no {@code obj} parameter associated.
+ */
+ public static final int EVENT_MT_STOP = 3;
+
+ private static final Point mViewSize = new Point(0, 0);
+
+ /**
+ * Constructs MultiTouchChannel instance.
+ */
+ public MultiTouchChannel(ControllerService service) {
+ super(service, Channel.MULTITOUCH_CHANNEL);
+ }
+
+ /**
+ * Sets size of the display view for emulated screen updates.
+ *
+ * @param width View width in pixels.
+ * @param height View height in pixels.
+ */
+ public void setViewSize(int width, int height) {
+ mViewSize.set(width, height);
+ }
+
+ /*
+ * Channel abstract implementation.
+ */
+
+ /**
+ * This method is invoked when this channel is fully connected with its
+ * counterpart in the emulator.
+ */
+ @Override
+ public void onEmulatorConnected() {
+ if (hasUiHandler()) {
+ enable();
+ notifyUiHandlers(EVENT_MT_START);
+ }
+ }
+
+ /**
+ * This method is invoked when this channel loses connection with its
+ * counterpart in the emulator.
+ */
+ @Override
+ public void onEmulatorDisconnected() {
+ if (hasUiHandler()) {
+ disable();
+ notifyUiHandlers(EVENT_MT_STOP);
+ }
+ }
+
+ /**
+ * A message has been received from the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg_data Packet received from the emulator.
+ */
+ @Override
+ public void onEmulatorMessage(int msg_type, ByteBuffer msg_data) {
+ switch (msg_type) {
+ case ProtocolConstants.MT_FB_UPDATE:
+ Message msg = Message.obtain();
+ msg.what = EVENT_FRAME_BUFFER;
+ msg.obj = msg_data;
+ postMessage(ProtocolConstants.MT_FB_ACK, (byte[]) null);
+ notifyUiHandlers(msg);
+ break;
+
+ default:
+ Log.e(TAG, "Unknown message type " + msg_type);
+ }
+ }
+
+ /**
+ * A query has been received from the emulator.
+ *
+ * @param query_id Identifies the query. This ID must be used when replying
+ * to the query.
+ * @param query_type Query type.
+ * @param query_data Query data.
+ */
+ @Override
+ public void onEmulatorQuery(int query_id, int query_type, ByteBuffer query_data) {
+ Loge("Unexpected query " + query_type + " in multi-touch");
+ sendQueryResponse(query_id, (byte[]) null);
+ }
+
+ /**
+ * Registers a new UI handler.
+ *
+ * @param uiHandler A non-null UI handler to register. Ignored if the UI
+ * handler is null or already registered.
+ */
+ @Override
+ public void addUiHandler(android.os.Handler uiHandler) {
+ final boolean first_handler = !hasUiHandler();
+ super.addUiHandler(uiHandler);
+ if (first_handler && isConnected()) {
+ enable();
+ notifyUiHandlers(EVENT_MT_START);
+ }
+ }
+
+ /**
+ * Unregisters an UI handler.
+ *
+ * @param uiHandler A non-null UI listener to unregister. Ignored if the
+ * listener is null or already registered.
+ */
+ @Override
+ public void removeUiHandler(android.os.Handler uiHandler) {
+ super.removeUiHandler(uiHandler);
+ if (isConnected() && !hasUiHandler()) {
+ disable();
+ }
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ mService.addError(log);
+ Log.e(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorChannel.java b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorChannel.java
new file mode 100755
index 000000000..ffc2fd03a
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/handlers/SensorChannel.java
@@ -0,0 +1,675 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.handlers;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.lib.Channel;
+import com.android.tools.sdkcontroller.lib.ProtocolConstants;
+import com.android.tools.sdkcontroller.service.ControllerService;
+
+/**
+ * Implements sensors emulation.
+ */
+public class SensorChannel extends Channel {
+
+ @SuppressWarnings("hiding")
+ private static String TAG = SensorChannel.class.getSimpleName();
+ @SuppressWarnings("hiding")
+ private static boolean DEBUG = false;
+ /**
+ * The target update time per sensor. Ignored if 0 or negative.
+ * Sensor updates that arrive faster than this delay are ignored.
+ * Ideally the emulator can be updated at up to 50 fps, however
+ * for average power devices something like 20 fps is more
+ * reasonable.
+ * Default value should match res/values/strings.xml > sensors_default_sample_rate.
+ */
+ private long mUpdateTargetMs = 1000/20; // 20 fps in milliseconds
+ /** Accumulates average update frequency. */
+ private long mGlobalAvgUpdateMs = 0;
+
+ /** Array containing monitored sensors. */
+ private final List<MonitoredSensor> mSensors = new ArrayList<MonitoredSensor>();
+ /** Sensor manager. */
+ private SensorManager mSenMan;
+
+ /*
+ * Messages exchanged with the UI.
+ */
+
+ /**
+ * Sensor "enabled by emulator" state has changed. Parameter {@code obj} is
+ * the {@link MonitoredSensor}.
+ */
+ public static final int SENSOR_STATE_CHANGED = 1;
+ /**
+ * Sensor display value has changed. Parameter {@code obj} is the
+ * {@link MonitoredSensor}.
+ */
+ public static final int SENSOR_DISPLAY_MODIFIED = 2;
+
+ /**
+ * Constructs SensorChannel instance.
+ *
+ * @param service Service context.
+ */
+ public SensorChannel(ControllerService service) {
+ super(service, Channel.SENSOR_CHANNEL);
+ mSenMan = (SensorManager) service.getSystemService(Context.SENSOR_SERVICE);
+ // Iterate through the available sensors, adding them to the array.
+ List<Sensor> sensors = mSenMan.getSensorList(Sensor.TYPE_ALL);
+ int cur_index = 0;
+ for (int n = 0; n < sensors.size(); n++) {
+ Sensor avail_sensor = sensors.get(n);
+
+ // There can be multiple sensors of the same type. We need only one.
+ if (!isSensorTypeAlreadyMonitored(avail_sensor.getType())) {
+ // The first sensor we've got for the given type is not
+ // necessarily the right one. So, use the default sensor
+ // for the given type.
+ Sensor def_sens = mSenMan.getDefaultSensor(avail_sensor.getType());
+ MonitoredSensor to_add = new MonitoredSensor(def_sens);
+ cur_index++;
+ mSensors.add(to_add);
+ if (DEBUG)
+ Log.d(TAG, String.format(
+ "Monitoring sensor #%02d: Name = '%s', Type = 0x%x",
+ cur_index, def_sens.getName(), def_sens.getType()));
+ }
+ }
+ }
+
+ /**
+ * Returns the list of sensors found on the device.
+ * The list is computed once by {@link #SensorChannel(ControllerService)}.
+ *
+ * @return A non-null possibly-empty list of sensors.
+ */
+ public List<MonitoredSensor> getSensors() {
+ return mSensors;
+ }
+
+ /**
+ * Set the target update delay throttling per-sensor, in milliseconds.
+ * <p/>
+ * For example setting it to 1000/50 means that updates for a <em>given</em> sensor
+ * faster than 50 fps is discarded.
+ *
+ * @param updateTargetMs 0 to disable throttling, otherwise a > 0 millisecond minimum
+ * between sensor updates.
+ */
+ public void setUpdateTargetMs(long updateTargetMs) {
+ mUpdateTargetMs = updateTargetMs;
+ }
+
+ /**
+ * Returns the actual average time in milliseconds between same-sensor updates.
+ *
+ * @return The actual average time in milliseconds between same-sensor updates or 0.
+ */
+ public long getActualUpdateMs() {
+ return mGlobalAvgUpdateMs;
+ }
+
+ /*
+ * Channel abstract implementation.
+ */
+
+ /**
+ * This method is invoked when this channel is fully connected with its
+ * counterpart in the emulator.
+ */
+ @Override
+ public void onEmulatorConnected() {
+ // Emulation is now possible. Note though that it will start only after
+ // emulator tells us so with SENSORS_START command.
+ enable();
+ }
+
+ /**
+ * This method is invoked when this channel loses connection with its
+ * counterpart in the emulator.
+ */
+ @Override
+ public void onEmulatorDisconnected() {
+ // Stop sensor event callbacks.
+ stopSensors();
+ }
+
+ /**
+ * A query has been received from the emulator.
+ *
+ * @param query_id Identifies the query. This ID should be used when
+ * replying to the query.
+ * @param query_type Query type.
+ * @param query_data Query data.
+ */
+ @Override
+ public void onEmulatorQuery(int query_id, int query_type, ByteBuffer query_data) {
+ switch (query_type) {
+ case ProtocolConstants.SENSORS_QUERY_LIST:
+ // Preallocate large response buffer.
+ ByteBuffer resp = ByteBuffer.allocate(1024);
+ resp.order(getEndian());
+ // Iterate through the list of monitored sensors, dumping them
+ // into the response buffer.
+ for (MonitoredSensor sensor : mSensors) {
+ // Entry for each sensor must contain:
+ // - an integer for its ID
+ // - a zero-terminated emulator-friendly name.
+ final byte[] name = sensor.getEmulatorFriendlyName().getBytes();
+ final int required_size = 4 + name.length + 1;
+ resp = ExpandIf(resp, required_size);
+ resp.putInt(sensor.getType());
+ resp.put(name);
+ resp.put((byte) 0);
+ }
+ // Terminating entry contains single -1 integer.
+ resp = ExpandIf(resp, 4);
+ resp.putInt(-1);
+ sendQueryResponse(query_id, resp);
+ return;
+
+ default:
+ Loge("Unknown query " + query_type);
+ return;
+ }
+ }
+
+ /**
+ * A message has been received from the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg_data Packet received from the emulator.
+ */
+ @Override
+ public void onEmulatorMessage(int msg_type, ByteBuffer msg_data) {
+ switch (msg_type) {
+ case ProtocolConstants.SENSORS_START:
+ Log.v(TAG, "Starting sensors emulation.");
+ startSensors();
+ break;
+ case ProtocolConstants.SENSORS_STOP:
+ Log.v(TAG, "Stopping sensors emulation.");
+ stopSensors();
+ break;
+ case ProtocolConstants.SENSORS_ENABLE:
+ String enable_name = new String(msg_data.array());
+ Log.v(TAG, "Enabling sensor: " + enable_name);
+ onEnableSensor(enable_name);
+ break;
+ case ProtocolConstants.SENSORS_DISABLE:
+ String disable_name = new String(msg_data.array());
+ Log.v(TAG, "Disabling sensor: " + disable_name);
+ onDisableSensor(disable_name);
+ break;
+ default:
+ Loge("Unknown message type " + msg_type);
+ break;
+ }
+ }
+
+ /**
+ * Handles 'enable' message.
+ *
+ * @param name Emulator-friendly name of a sensor to enable, or "all" to
+ * enable all sensors.
+ */
+ private void onEnableSensor(String name) {
+ if (name.contentEquals("all")) {
+ // Enable all sensors.
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.enableSensor();
+ }
+ } else {
+ // Lookup sensor by emulator-friendly name.
+ final MonitoredSensor sensor = getSensorByEFN(name);
+ if (sensor != null) {
+ sensor.enableSensor();
+ }
+ }
+ }
+
+ /**
+ * Handles 'disable' message.
+ *
+ * @param name Emulator-friendly name of a sensor to disable, or "all" to
+ * disable all sensors.
+ */
+ private void onDisableSensor(String name) {
+ if (name.contentEquals("all")) {
+ // Disable all sensors.
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.disableSensor();
+ }
+ } else {
+ // Lookup sensor by emulator-friendly name.
+ MonitoredSensor sensor = getSensorByEFN(name);
+ if (sensor != null) {
+ sensor.disableSensor();
+ }
+ }
+ }
+
+ /**
+ * Start listening to all monitored sensors.
+ */
+ private void startSensors() {
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.startListening();
+ }
+ }
+
+ /**
+ * Stop listening to all monitored sensors.
+ */
+ private void stopSensors() {
+ for (MonitoredSensor sensor : mSensors) {
+ sensor.stopListening();
+ }
+ }
+
+ /***************************************************************************
+ * Internals
+ **************************************************************************/
+
+ /**
+ * Checks if a sensor for the given type is already monitored.
+ *
+ * @param type Sensor type (one of the Sensor.TYPE_XXX constants)
+ * @return true if a sensor for the given type is already monitored, or
+ * false if the sensor is not monitored.
+ */
+ private boolean isSensorTypeAlreadyMonitored(int type) {
+ for (MonitoredSensor sensor : mSensors) {
+ if (sensor.getType() == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Looks up a monitored sensor by its emulator-friendly name.
+ *
+ * @param name Emulator-friendly name to look up the monitored sensor for.
+ * @return Monitored sensor for the fiven name, or null if sensor was not
+ * found.
+ */
+ private MonitoredSensor getSensorByEFN(String name) {
+ for (MonitoredSensor sensor : mSensors) {
+ if (sensor.mEmulatorFriendlyName.contentEquals(name)) {
+ return sensor;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Encapsulates a sensor that is being monitored. To monitor sensor changes
+ * each monitored sensor registers with sensor manager as a sensor listener.
+ * To control sensor monitoring from the UI, each monitored sensor has two
+ * UI controls associated with it: - A check box (named after sensor) that
+ * can be used to enable, or disable listening to the sensor changes. - A
+ * text view where current sensor value is displayed.
+ */
+ public class MonitoredSensor {
+ /** Sensor to monitor. */
+ private final Sensor mSensor;
+ /** The sensor name to display in the UI. */
+ private String mUiName = "";
+ /** Text view displaying the value of the sensor. */
+ private String mValue = null;
+ /** Emulator-friendly name for the sensor. */
+ private String mEmulatorFriendlyName;
+ /** Formats string to show in the TextView. */
+ private String mTextFmt;
+ /** Sensor values. */
+ private float[] mValues = new float[3];
+ /**
+ * Enabled state. This state is controlled by the emulator, that
+ * maintains its own list of sensors. So, if a sensor is missing, or is
+ * disabled in the emulator, it should be disabled in this application.
+ */
+ private boolean mEnabledByEmulator = false;
+ /** User-controlled enabled state. */
+ private boolean mEnabledByUser = true;
+ /** Sensor event listener for this sensor. */
+ private final OurSensorEventListener mListener = new OurSensorEventListener();
+
+ /**
+ * Constructs MonitoredSensor instance, and register the listeners.
+ *
+ * @param sensor Sensor to monitor.
+ */
+ MonitoredSensor(Sensor sensor) {
+ mSensor = sensor;
+ mEnabledByUser = true;
+
+ // Set appropriate sensor name depending on the type. Unfortunately,
+ // we can't really use sensor.getName() here, since the value it
+ // returns (although resembles the purpose) is a bit vaguer than it
+ // should be. Also choose an appropriate format for the strings that
+ // display sensor's value.
+ switch (sensor.getType()) {
+ case Sensor.TYPE_ACCELEROMETER:
+ mUiName = "Accelerometer";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "acceleration";
+ break;
+ case 9: // Sensor.TYPE_GRAVITY is missing in API 7
+ mUiName = "Gravity";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "gravity";
+ break;
+ case Sensor.TYPE_GYROSCOPE:
+ mUiName = "Gyroscope";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "gyroscope";
+ break;
+ case Sensor.TYPE_LIGHT:
+ mUiName = "Light";
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "light";
+ break;
+ case 10: // Sensor.TYPE_LINEAR_ACCELERATION is missing in API 7
+ mUiName = "Linear acceleration";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "linear-acceleration";
+ break;
+ case Sensor.TYPE_MAGNETIC_FIELD:
+ mUiName = "Magnetic field";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "magnetic-field";
+ break;
+ case Sensor.TYPE_ORIENTATION:
+ mUiName = "Orientation";
+ mTextFmt = "%+03.0f %+03.0f %+03.0f";
+ mEmulatorFriendlyName = "orientation";
+ break;
+ case Sensor.TYPE_PRESSURE:
+ mUiName = "Pressure";
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "pressure";
+ break;
+ case Sensor.TYPE_PROXIMITY:
+ mUiName = "Proximity";
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "proximity";
+ break;
+ case 11: // Sensor.TYPE_ROTATION_VECTOR is missing in API 7
+ mUiName = "Rotation";
+ mTextFmt = "%+.2f %+.2f %+.2f";
+ mEmulatorFriendlyName = "rotation";
+ break;
+ case Sensor.TYPE_TEMPERATURE:
+ mUiName = "Temperature";
+ mTextFmt = "%.0f";
+ mEmulatorFriendlyName = "temperature";
+ break;
+ default:
+ mUiName = "<Unknown>";
+ mTextFmt = "N/A";
+ mEmulatorFriendlyName = "unknown";
+ if (DEBUG) Loge("Unknown sensor type " + mSensor.getType() +
+ " for sensor " + mSensor.getName());
+ break;
+ }
+ }
+
+ /**
+ * Get name for this sensor to display.
+ *
+ * @return Name for this sensor to display.
+ */
+ public String getUiName() {
+ return mUiName;
+ }
+
+ /**
+ * Gets current sensor value to display.
+ *
+ * @return Current sensor value to display.
+ */
+ public String getValue() {
+ if (mValue == null) {
+ float[] values = mValues;
+ mValue = String.format(mTextFmt, values[0], values[1], values[2]);
+ }
+ return mValue == null ? "??" : mValue;
+ }
+
+ /**
+ * Checks if monitoring of this this sensor has been enabled by
+ * emulator.
+ *
+ * @return true if monitoring of this this sensor has been enabled by
+ * emulator, or false if emulator didn't enable this sensor.
+ */
+ public boolean isEnabledByEmulator() {
+ return mEnabledByEmulator;
+ }
+
+ /**
+ * Checks if monitoring of this this sensor has been enabled by user.
+ *
+ * @return true if monitoring of this this sensor has been enabled by
+ * user, or false if user didn't enable this sensor.
+ */
+ public boolean isEnabledByUser() {
+ return mEnabledByUser;
+ }
+
+ /**
+ * Handles checked state change for the associated CheckBox. If check
+ * box is checked we will register sensor change listener. If it is
+ * unchecked, we will unregister sensor change listener.
+ */
+ public void onCheckedChanged(boolean isChecked) {
+ mEnabledByUser = isChecked;
+ if (isChecked) {
+ startListening();
+ } else {
+ stopListening();
+ }
+ }
+
+ /**
+ * Gets sensor type.
+ *
+ * @return Sensor type as one of the Sensor.TYPE_XXX constants.
+ */
+ private int getType() {
+ return mSensor.getType();
+ }
+
+ /**
+ * Gets sensor's emulator-friendly name.
+ *
+ * @return Sensor's emulator-friendly name.
+ */
+ private String getEmulatorFriendlyName() {
+ return mEmulatorFriendlyName;
+ }
+
+ /**
+ * Starts monitoring the sensor.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void startListening() {
+ if (mEnabledByEmulator && mEnabledByUser) {
+ if (DEBUG) Log.d(TAG, "+++ Sensor " + getEmulatorFriendlyName() + " is started.");
+ mSenMan.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_FASTEST);
+ }
+ }
+
+ /**
+ * Stops monitoring the sensor.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void stopListening() {
+ if (DEBUG) Log.d(TAG, "--- Sensor " + getEmulatorFriendlyName() + " is stopped.");
+ mSenMan.unregisterListener(mListener);
+ }
+
+ /**
+ * Enables sensor events.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void enableSensor() {
+ if (DEBUG) Log.d(TAG, ">>> Sensor " + getEmulatorFriendlyName() + " is enabled.");
+ mEnabledByEmulator = true;
+ mValue = null;
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_STATE_CHANGED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ /**
+ * Disables sensor events.
+ * NOTE: This method is called from outside of the UI thread.
+ */
+ private void disableSensor() {
+ if (DEBUG) Log.w(TAG, "<<< Sensor " + getEmulatorFriendlyName() + " is disabled.");
+ mEnabledByEmulator = false;
+ mValue = "Disabled by emulator";
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_STATE_CHANGED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ private class OurSensorEventListener implements SensorEventListener {
+ /** Last update's time-stamp in local thread millisecond time. */
+ private long mLastUpdateTS = 0;
+ /** Last display update time-stamp. */
+ private long mLastDisplayTS = 0;
+ /** Preallocated buffer for change notification message. */
+ private final ByteBuffer mChangeMsg = ByteBuffer.allocate(64);
+
+ /**
+ * Handles "sensor changed" event.
+ * This is an implementation of the SensorEventListener interface.
+ */
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ long now = SystemClock.elapsedRealtime();
+
+ long deltaMs = 0;
+ if (mLastUpdateTS != 0) {
+ deltaMs = now - mLastUpdateTS;
+ if (mUpdateTargetMs > 0 && deltaMs < mUpdateTargetMs) {
+ // New sample is arriving too fast. Discard it.
+ return;
+ }
+ }
+
+ // Format and post message for the emulator.
+ float[] values = event.values;
+ final int len = values.length;
+
+ mChangeMsg.order(getEndian());
+ mChangeMsg.position(0);
+ mChangeMsg.putInt(getType());
+ mChangeMsg.putFloat(values[0]);
+ if (len > 1) {
+ mChangeMsg.putFloat(values[1]);
+ if (len > 2) {
+ mChangeMsg.putFloat(values[2]);
+ }
+ }
+ postMessage(ProtocolConstants.SENSORS_SENSOR_EVENT, mChangeMsg);
+
+ // Computes average update time for this sensor and average globally.
+ if (mLastUpdateTS != 0) {
+ if (mGlobalAvgUpdateMs != 0) {
+ mGlobalAvgUpdateMs = (mGlobalAvgUpdateMs + deltaMs) / 2;
+ } else {
+ mGlobalAvgUpdateMs = deltaMs;
+ }
+ }
+ mLastUpdateTS = now;
+
+ // Update the UI for the sensor, with a static throttling of 10 fps max.
+ if (hasUiHandler()) {
+ if (mLastDisplayTS != 0) {
+ long uiDeltaMs = now - mLastDisplayTS;
+ if (uiDeltaMs < 1000 / 4 /* 4fps in ms */) {
+ // Skip this UI update
+ return;
+ }
+ }
+ mLastDisplayTS = now;
+
+ mValues[0] = values[0];
+ if (len > 1) {
+ mValues[1] = values[1];
+ if (len > 2) {
+ mValues[2] = values[2];
+ }
+ }
+ mValue = null;
+
+ Message msg = Message.obtain();
+ msg.what = SENSOR_DISPLAY_MODIFIED;
+ msg.obj = MonitoredSensor.this;
+ notifyUiHandlers(msg);
+ }
+
+ if (DEBUG) {
+ long now2 = SystemClock.elapsedRealtime();
+ long processingTimeMs = now2 - now;
+ Log.d(TAG, String.format("glob %d - local %d > target %d - processing %d -- %s",
+ mGlobalAvgUpdateMs, deltaMs, mUpdateTargetMs, processingTimeMs,
+ mSensor.getName()));
+ }
+ }
+
+ /**
+ * Handles "sensor accuracy changed" event.
+ * This is an implementation of the SensorEventListener interface.
+ */
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ }
+ }
+ } // MonitoredSensor
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ mService.addError(log);
+ Log.e(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java
new file mode 100644
index 000000000..639f4cfd4
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Channel.java
@@ -0,0 +1,795 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.lib;
+
+import android.os.Message;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.service.ControllerService;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Encapsulates basics of a connection with the emulator.
+ * This class must be used as a base class for all the channelss that provide
+ * particular type of emulation (such as sensors, multi-touch, etc.)
+ * <p/>
+ * Essentially, Channel is an implementation of a particular emulated functionality,
+ * that defines logical format of the data transferred between the emulator and
+ * SDK controller. For instance, "sensors" is a channel that emulates sensors,
+ * and transfers sensor value changes from the device to the emulator. "Multi-touch"
+ * is a channel that supports multi-touch emulation, and transfers multi-touch
+ * events to the emulator, while receiving frame buffer updates from the emulator.
+ * <p/>
+ * Besides connection with the emulator, each channel may contain one or more UI
+ * components associated with it. This class provides some basics for UI support,
+ * including:
+ * <p/>
+ * - Providing a way to register / unregister a UI component with the channel.
+ * <p/>
+ * - Implementing posting of messages to emulator in opposite to direct message
+ * sent. This is due to requirement that UI threads are prohibited from doing
+ * network I/O.
+ */
+public abstract class Channel {
+
+ /**
+ * Encapsulates a message posted to be sent to the emulator from a worker
+ * thread. This class is used to describe a message that is posted in UI
+ * thread, and then picked up in the worker thread.
+ */
+ private class SdkControllerMessage {
+ /** Message type. */
+ private int mMessageType;
+ /** Message data (can be null). */
+ private byte[] mMessage;
+ /** Message data size */
+ private int mMessageSize;
+
+ /**
+ * Construct message from an array.
+ *
+ * @param type Message type.
+ * @param message Message data. Message data size is defined by size of
+ * the array.
+ */
+ public SdkControllerMessage(int type, byte[] message) {
+ mMessageType = type;
+ mMessage = message;
+ mMessageSize = (message != null) ? message.length : 0;
+ }
+
+ /**
+ * Construct message from a ByteBuffer.
+ *
+ * @param type Message type.
+ * @param message Message data. Message data size is defined by
+ * position() property of the ByteBuffer.
+ */
+ public SdkControllerMessage(int type, ByteBuffer message) {
+ mMessageType = type;
+ if (message != null) {
+ mMessage = message.array();
+ mMessageSize = message.position();
+ } else {
+ mMessage = null;
+ mMessageSize = 0;
+ }
+ }
+
+ /**
+ * Gets message type.
+
+ *
+ * @return Message type.
+ */
+ public int getMessageType() {
+ return mMessageType;
+ }
+
+ /**
+ * Gets message buffer.
+ *
+ * @return Message buffer.
+ */
+ public byte[] getMessage() {
+ return mMessage;
+ }
+
+ /**
+ * Gets message buffer size.
+ *
+ * @return Message buffer size.
+ */
+ public int getMessageSize() {
+ return mMessageSize;
+ }
+ } // SdkControllerMessage
+
+ /*
+ * Names for currently implemented SDK controller channels.
+ */
+
+ /** Name for a channel that handles sensors emulation */
+ public static final String SENSOR_CHANNEL = "sensors";
+ /** Name for a channel that handles multi-touch emulation */
+ public static final String MULTITOUCH_CHANNEL = "multi-touch";
+
+ /*
+ * Types of messages internally used by Channel class.
+ */
+
+ /** Service-side emulator is connected. */
+ private static final int MSG_CONNECTED = -1;
+ /** Service-side emulator is disconnected. */
+ private static final int MSG_DISCONNECTED = -2;
+ /** Service-side emulator is enabled. */
+ private static final int MSG_ENABLED = -3;
+ /** Service-side emulator is disabled. */
+ private static final int MSG_DISABLED = -4;
+
+ /** Tag for logging messages. */
+ private static final String TAG = "SdkControllerChannel";
+ /** Controls debug log. */
+ private static final boolean DEBUG = false;
+
+ /** Service that has created this object. */
+ protected ControllerService mService;
+
+ /*
+ * Socket stuff.
+ */
+
+ /** Socket to use to to communicate with the emulator. */
+ private Socket mSocket = null;
+ /** Channel name ("sensors", "multi-touch", etc.) */
+ private String mChannelName;
+ /** Endianness of data transferred in this channel. */
+ private ByteOrder mEndian;
+
+ /*
+ * Message posting support.
+ */
+
+ /** Total number of messages posted in this channel */
+ private final AtomicInteger mMsgCount = new AtomicInteger(0);
+ /** Flags whether or not message thread is running. */
+ private volatile boolean mRunMsgQueue = true;
+ /** Queue of messages pending transmission. */
+ private final BlockingQueue<SdkControllerMessage>
+ mMsgQueue = new LinkedBlockingQueue<SdkControllerMessage>();
+ /** Message thread */
+ private final Thread mMsgThread;
+
+ /*
+ * UI support.
+ */
+
+ /** Lists UI handlers attached to this channel. */
+ private final List<android.os.Handler> mUiHandlers = new ArrayList<android.os.Handler>();
+
+ /*
+ * Abstract methods.
+ */
+
+ /**
+ * This method is invoked when this channel is fully connected with its
+ * counterpart in the emulator.
+ */
+ public abstract void onEmulatorConnected();
+
+ /**
+ * This method is invoked when this channel loses connection with its
+ * counterpart in the emulator.
+ */
+ public abstract void onEmulatorDisconnected();
+
+ /**
+ * A message has been received from the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg_data Message data. Message data size is defined by the length
+ * of the array wrapped by the ByteBuffer.
+ */
+ public abstract void onEmulatorMessage(int msg_type, ByteBuffer msg_data);
+
+ /**
+ * A query has been received from the emulator.
+ *
+ * @param query_id Identifies the query. This ID must be used when replying
+ * to the query.
+ * @param query_type Query type.
+ * @param query_data Query data. Query data size is defined by the length of
+ * the array wrapped by the ByteBuffer.
+ */
+ public abstract void onEmulatorQuery(int query_id, int query_type, ByteBuffer query_data);
+
+ /*
+ * Channel implementation.
+ */
+
+ /**
+ * Constructs Channel instance.
+ *
+ * @param name Channel name.
+ */
+ public Channel(ControllerService service, String name) {
+ mService = service;
+ mChannelName = name;
+ // Start the worker thread for posted messages.
+ mMsgThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.d(TAG, "MsgThread.started-" + mChannelName);
+ while (mRunMsgQueue) {
+ try {
+ SdkControllerMessage msg = mMsgQueue.take();
+ if (msg != null) {
+ sendMessage(
+ msg.getMessageType(), msg.getMessage(), msg.getMessageSize());
+ mMsgCount.incrementAndGet();
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "MsgThread-" + mChannelName, e);
+ }
+ }
+ if (DEBUG) Log.d(TAG, "MsgThread.terminate-" + mChannelName);
+ }
+ }, "MsgThread-" + name);
+ mMsgThread.start();
+ if (DEBUG) Log.d(TAG, "Channel is constructed for " + mChannelName);
+ }
+
+ /**
+ * Gets name for this channel.
+ *
+ * @return Emulator name.
+ */
+ public String getChannelName() {
+ return mChannelName;
+ }
+
+ /**
+ * Gets endianness for this channel.
+ *
+ * @return Channel endianness.
+ */
+ public ByteOrder getEndian() {
+ return mEndian;
+ }
+
+ /**
+ * Gets number of messages sent via postMessage method.
+ *
+ * @return Number of messages sent via postMessage method.
+ */
+ public int getMsgSentCount() {
+ return mMsgCount.get();
+ }
+
+ /**
+ * Checks if this channel is connected with the emulator.
+ *
+ * @return true if this channel is connected with the emulator, or false if it is
+ * not connected.
+ */
+ public boolean isConnected() {
+ // Use local copy of the socket, ensuring it's not going to NULL while
+ // we're working with it. If it gets closed, while we're in the middle
+ // of data transfer - it's OK, since it will produce an exception, and
+ // the caller will gracefully handle it.
+ //
+ // Same technique is used everywhere in this class where mSocket member
+ // is touched.
+ Socket socket = mSocket;
+ return socket != null && socket.isConnected();
+ }
+
+ /**
+ * Establishes connection with the emulator. This method is called by Connection
+ * object when emulator successfully connects to this channel, or this channel
+ * gets registered, and there is a pending socket connection for it.
+ *
+ * @param socket Channel connection socket.
+ */
+ public void connect(Socket socket) {
+ mSocket = socket;
+ mEndian = socket.getEndian();
+ Logv("Channel " + mChannelName + " is now connected with the emulator.");
+ // Notify the emulator that connection is established.
+ sendMessage(MSG_CONNECTED, (byte[]) null);
+
+ // Let the derived class know that emulator is connected, and start the
+ // I/O loop in which we will receive data from the emulator. Note that
+ // we start the loop after onEmulatorConnected call, since we don't want
+ // to start dispatching messages before the derived class could set
+ // itself up for receiving them.
+ onEmulatorConnected();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ runIOLooper();
+ }
+ }, "ChannelIoLoop").start();
+ mService.notifyStatusChanged();
+ }
+
+ /**
+ * Disconnects this channel from the emulator.
+ *
+ * @return true if this channel has been disconnected in this call, or false if
+ * channel has been already disconnected when this method has been called.
+ */
+ public boolean disconnect() {
+ // This is the only place in this class where we will null the
+ // socket object. Since this method can be called concurrently from
+ // different threads, lets do this under the lock.
+ Socket socket;
+ synchronized (this) {
+ socket = mSocket;
+ mSocket = null;
+ }
+ if (socket != null) {
+ // Notify the emulator about channel disconnection before we close
+ // the communication socket.
+ try {
+ sendMessage(socket, MSG_DISCONNECTED, null, 0);
+ } catch (IOException e) {
+ // Ignore I/O exception at this point. We don't care about
+ // it, since the socket is being closed anyways.
+ }
+ // This will eventually stop I/O looper thread.
+ socket.close();
+ mService.notifyStatusChanged();
+ }
+ return socket != null;
+ }
+
+ /**
+ * Enables the emulation. Typically, this method is called for channels that are
+ * dependent on UI to handle the emulation. For instance, multi-touch emulation is
+ * disabled until at least one UI component is attached to the channel. So, for
+ * multi-touch emulation this method is called when UI gets attached to the channel.
+ */
+ public void enable() {
+ postMessage(MSG_ENABLED, (byte[]) null);
+ mService.notifyStatusChanged();
+ }
+
+ /**
+ * Disables the emulation. Just the opposite to enable(). For multi-touch this
+ * method is called when UI detaches from the channel.
+ */
+ public void disable() {
+ postMessage(MSG_DISABLED, (byte[]) null);
+ mService.notifyStatusChanged();
+ }
+
+ /**
+ * Sends message to the emulator.
+ *
+ * @param socket Socket to send the message to.
+ * @param msg_type Message type.
+ * @param msg Message data to send.
+ * @param len Byte size of message data.
+ * @throws IOException
+ */
+ private void sendMessage(Socket socket, int msg_type, byte[] msg, int len)
+ throws IOException {
+ // In async environment we must have message header and message data in
+ // one block to prevent messages from other threads getting between the
+ // header and the data. So, we can't sent header, and then the data. We
+ // must combine them in one data block instead.
+ ByteBuffer bb = ByteBuffer.allocate(ProtocolConstants.MESSAGE_HEADER_SIZE + len);
+ bb.order(mEndian);
+
+ // Initialize message header.
+ bb.putInt(ProtocolConstants.PACKET_SIGNATURE);
+ bb.putInt(ProtocolConstants.MESSAGE_HEADER_SIZE + len);
+ bb.putInt(ProtocolConstants.PACKET_TYPE_MESSAGE);
+ bb.putInt(msg_type);
+
+ // Save message data (if there is any).
+ if (len != 0) {
+ bb.put(msg, 0, len);
+ }
+
+ socket.send(bb.array());
+ }
+
+ /**
+ * Sends message to the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg Message data to send. Message size is defined by the size of
+ * the array.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendMessage(int msg_type, byte[] msg, int msg_len) {
+ try {
+ Socket socket = mSocket;
+ if (socket != null) {
+ sendMessage(socket, msg_type, msg, msg_len);
+ return true;
+ } else {
+ Logw("sendMessage is called on disconnected Channel " + mChannelName);
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + " in sendMessage for Channel " + mChannelName);
+ onIoFailure();
+ }
+ return false;
+ }
+
+ /**
+ * Sends message to the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg Message data to send. Message size is defined by the size of
+ * the array.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendMessage(int msg_type, byte[] msg) {
+ try {
+ Socket socket = mSocket;
+ if (socket != null) {
+ if (msg != null) {
+ sendMessage(socket, msg_type, msg, msg.length);
+ } else {
+ sendMessage(socket, msg_type, null, 0);
+ }
+ return true;
+ } else {
+ Logw("sendMessage is called on disconnected Channel " + mChannelName);
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + " in sendMessage for Channel " + mChannelName);
+ onIoFailure();
+ }
+ return false;
+ }
+
+ /**
+ * Sends message to the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg Message data to send. Message size is defined by the
+ * position() property of the ByteBuffer.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendMessage(int msg_type, ByteBuffer msg) {
+ try {
+ Socket socket = mSocket;
+ if (socket != null) {
+ if (msg != null) {
+ sendMessage(socket, msg_type, msg.array(), msg.position());
+ } else {
+ sendMessage(socket, msg_type, null, 0);
+ }
+ return true;
+ } else {
+ Logw("sendMessage is called on disconnected Channel " + mChannelName);
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + " in sendMessage for Channel " + mChannelName);
+ onIoFailure();
+ }
+ return false;
+ }
+
+ /**
+ * Posts message to the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg Message data to post. Message size is defined by the size of
+ * the array.
+ */
+ public void postMessage(int msg_type, byte[] msg) {
+ try {
+ mMsgQueue.put(new SdkControllerMessage(msg_type, msg));
+ } catch (InterruptedException e) {
+ Log.e(TAG, "mMessageQueue.put", e);
+ }
+ }
+
+ /**
+ * Posts message to the emulator.
+ *
+ * @param msg_type Message type.
+ * @param msg Message data to post. Message size is defined by the
+ * position() property of the ByteBuffer.
+ */
+ public void postMessage(int msg_type, ByteBuffer msg) {
+ try {
+ mMsgQueue.put(new SdkControllerMessage(msg_type, msg));
+ } catch (InterruptedException e) {
+ Log.e(TAG, "mMessageQueue.put", e);
+ }
+ }
+
+ /**
+ * Sends query response to the emulator.
+ *
+ * @param query_id Query identifier.
+ * @param qresp Response to the query.
+ * @param len Byte size of query response data.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendQueryResponse(int query_id, byte[] qresp, int len) {
+ // Just like with messages, we must combine header and data in a single
+ // transmitting block.
+ ByteBuffer bb = ByteBuffer.allocate(ProtocolConstants.QUERY_RESP_HEADER_SIZE + len);
+ bb.order(mEndian);
+
+ // Initialize response header.
+ bb.putInt(ProtocolConstants.PACKET_SIGNATURE);
+ bb.putInt(ProtocolConstants.QUERY_RESP_HEADER_SIZE + len);
+ bb.putInt(ProtocolConstants.PACKET_TYPE_QUERY_RESPONSE);
+ bb.putInt(query_id);
+
+ // Save response data (if there is any).
+ if (qresp != null && len != 0) {
+ bb.put(qresp, 0, len);
+ }
+
+ // Send the response.
+ try {
+ Socket socket = mSocket;
+ if (socket != null) {
+ socket.send(bb.array());
+ return true;
+ } else {
+ Logw("sendQueryResponse is called on disconnected Channel "
+ + mChannelName);
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + " in sendQueryResponse for Channel " + mChannelName);
+ onIoFailure();
+ }
+ return false;
+ }
+
+ /**
+ * Sends query response to the emulator.
+ *
+ * @param query_id Query identifier.
+ * @param qresp Response to the query. Query response size is defined by the
+ * size of the array.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendQueryResponse(int query_id, byte[] qresp) {
+ return (qresp != null) ? sendQueryResponse(query_id, qresp, qresp.length) :
+ sendQueryResponse(query_id, null, 0);
+ }
+
+ /**
+ * Sends query response to the emulator.
+ *
+ * @param query_id Query identifier.
+ * @param qresp Response to the query. Query response size is defined by the
+ * position() property of the ByteBuffer.
+ * @return true on success, or false if data transmission has failed.
+ */
+ public boolean sendQueryResponse(int query_id, ByteBuffer qresp) {
+ return (qresp != null) ? sendQueryResponse(query_id, qresp.array(), qresp.position()) :
+ sendQueryResponse(query_id, null, 0);
+ }
+
+ /**
+ * Handles an I/O failure occurred in the channel.
+ */
+ private void onIoFailure() {
+ // All I/O failures cause disconnection.
+ if (disconnect()) {
+ // Success of disconnect() indicates that I/O failure is not the
+ // result of a disconnection request, but is in deed an I/O
+ // failure. Report lost connection to the derived class.
+ Loge("Connection with the emulator has been lost in Channel " + mChannelName);
+ onEmulatorDisconnected();
+ }
+ }
+
+ /**
+ * Loops on the local socket, handling connection attempts.
+ */
+ private void runIOLooper() {
+ if (DEBUG) Log.d(TAG, "In I/O looper for Channel " + mChannelName);
+ // Initialize byte buffer large enough to receive packet header.
+ ByteBuffer header = ByteBuffer.allocate(ProtocolConstants.PACKET_HEADER_SIZE);
+ header.order(mEndian);
+ try {
+ // Since disconnection (which will null the mSocket) can be
+ // requested from outside of this thread, it's simpler just to make
+ // a copy of mSocket here, and work with that copy. Otherwise we
+ // will have to go through a complex synchronization algorithm that
+ // would decrease performance on normal runs. If socket gets closed
+ // while we're in the middle of transfer, an exception will occur,
+ // which we will catch and handle properly.
+ Socket socket = mSocket;
+ while (socket != null) {
+ // Reset header position.
+ header.position(0);
+ // This will receive total packet size + packet type.
+ socket.receive(header.array());
+ // First - signature.
+ final int signature = header.getInt();
+ assert signature == ProtocolConstants.PACKET_SIGNATURE;
+ // Next - packet size (including header).
+ int remains = header.getInt() - ProtocolConstants.PACKET_HEADER_SIZE;
+ // After the size comes packet type.
+ final int packet_type = header.getInt();
+
+ // Get the remainder of the data, and dispatch the packet to
+ // an appropriate handler.
+ switch (packet_type) {
+ case ProtocolConstants.PACKET_TYPE_MESSAGE:
+ // Read message header (one int: message type).
+ final int ext = ProtocolConstants.MESSAGE_HEADER_SIZE - ProtocolConstants.PACKET_HEADER_SIZE;
+ header.position(0);
+ socket.receive(header.array(), ext);
+ final int msg_type = header.getInt();
+
+ // Read message data.
+ remains -= ext;
+ final ByteBuffer msg_data = ByteBuffer.allocate(remains);
+ msg_data.order(mEndian);
+ socket.receive(msg_data.array());
+
+ // Dispatch message for handling.
+ onEmulatorMessage(msg_type, msg_data);
+ break;
+
+ case ProtocolConstants.PACKET_TYPE_QUERY:
+ // Read query ID and query type.
+ final int extq = ProtocolConstants.QUERY_HEADER_SIZE - ProtocolConstants.PACKET_HEADER_SIZE;
+ header.position(0);
+ socket.receive(header.array(), extq);
+ final int query_id = header.getInt();
+ final int query_type = header.getInt();
+
+ // Read query data.
+ remains -= extq;
+ final ByteBuffer query_data = ByteBuffer.allocate(remains);
+ query_data.order(mEndian);
+ socket.receive(query_data.array());
+
+ // Dispatch query for handling.
+ onEmulatorQuery(query_id, query_type, query_data);
+ break;
+
+ default:
+ // Unknown packet type. Just discard the remainder
+ // of the packet
+ Loge("Unknown packet type " + packet_type + " in Channel "
+ + mChannelName);
+ final byte[] discard_data = new byte[remains];
+ socket.receive(discard_data);
+ break;
+ }
+ socket = mSocket;
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + " in I/O looper for Channel " + mChannelName);
+ onIoFailure();
+ }
+ if (DEBUG) Log.d(TAG, "Exiting I/O looper for Channel " + mChannelName);
+ }
+
+ /**
+ * Indicates any UI handler is currently registered with the channel. If no UI
+ * is displaying the channel's state, maybe the channel can skip UI related tasks.
+ *
+ * @return True if there's at least one UI handler registered.
+ */
+ public boolean hasUiHandler() {
+ return !mUiHandlers.isEmpty();
+ }
+
+ /**
+ * Registers a new UI handler.
+ *
+ * @param uiHandler A non-null UI handler to register. Ignored if the UI
+ * handler is null or already registered.
+ */
+ public void addUiHandler(android.os.Handler uiHandler) {
+ assert uiHandler != null;
+ if (uiHandler != null) {
+ if (!mUiHandlers.contains(uiHandler)) {
+ mUiHandlers.add(uiHandler);
+ }
+ }
+ }
+
+ /**
+ * Unregisters an UI handler.
+ *
+ * @param uiHandler A non-null UI listener to unregister. Ignored if the
+ * listener is null or already registered.
+ */
+ public void removeUiHandler(android.os.Handler uiHandler) {
+ assert uiHandler != null;
+ mUiHandlers.remove(uiHandler);
+ }
+
+ /**
+ * Protected method to be used by handlers to send an event to all UI
+ * handlers.
+ *
+ * @param event An integer event code with no specific parameters. To be
+ * defined by the handler itself.
+ */
+ protected void notifyUiHandlers(int event) {
+ for (android.os.Handler uiHandler : mUiHandlers) {
+ uiHandler.sendEmptyMessage(event);
+ }
+ }
+
+ /**
+ * Protected method to be used by handlers to send an event to all UI
+ * handlers.
+ *
+ * @param msg An event with parameters. To be defined by the handler itself.
+ */
+ protected void notifyUiHandlers(Message msg) {
+ for (android.os.Handler uiHandler : mUiHandlers) {
+ uiHandler.sendMessage(msg);
+ }
+ }
+
+ /**
+ * A helper routine that expands ByteBuffer to contain given number of extra
+ * bytes.
+ *
+ * @param buff Buffer to expand.
+ * @param extra Number of bytes that are required to be available in the
+ * buffer after current position()
+ * @return ByteBuffer, containing required number of available bytes.
+ */
+ public ByteBuffer ExpandIf(ByteBuffer buff, int extra) {
+ if (extra <= buff.remaining()) {
+ return buff;
+ }
+ ByteBuffer ret = ByteBuffer.allocate(buff.position() + extra);
+ ret.order(buff.order());
+ ret.put(buff.array(), 0, buff.position());
+ return ret;
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ mService.addError(log);
+ Log.e(TAG, log);
+ }
+
+ private void Logw(String log) {
+ Log.w(TAG, log);
+ }
+
+ private void Logv(String log) {
+ Log.v(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java
new file mode 100644
index 000000000..cb5086905
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Connection.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.lib;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+import android.util.Log;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+
+import com.android.tools.sdkcontroller.lib.Channel;
+import com.android.tools.sdkcontroller.service.ControllerService;
+
+/**
+ * Encapsulates a connection between SdkController service and the emulator. On
+ * the device side, the connection is bound to the UNIX-domain socket named
+ * 'android.sdk.controller'. On the emulator side the connection is established
+ * via TCP port that is used to forward I/O traffic on the host machine to
+ * 'android.sdk.controller' socket on the device. Typically, the port forwarding
+ * can be enabled using adb command:
+ * <p/>
+ * 'adb forward tcp:<TCP port number> localabstract:android.sdk.controller'
+ * <p/>
+ * The way communication between the emulator and SDK controller service works
+ * is as follows:
+ * <p/>
+ * 1. Both sides, emulator and the service have components that implement a particular
+ * type of emulation. For instance, AndroidSensorsPort in the emulator, and
+ * SensorChannel in the application implement sensors emulation.
+ * Emulation channels are identified by unique names. For instance, sensor emulation
+ * is done via "sensors" channel, multi-touch emulation is done via "multi-touch"
+ * channel, etc.
+ * <p/>
+ * 2. Channels are connected to emulator via separate socket instance (though all
+ * of the connections share the same socket address).
+ * <p/>
+ * 3. Connection is initiated by the emulator side, while the service provides
+ * its side (a channel) that implement functionality and exchange protocol required
+ * by the requested type of emulation.
+ * <p/>
+ * Given that, the main responsibilities of this class are:
+ * <p/>
+ * 1. Bind to "android.sdk.controller" socket, listening to emulator connections.
+ * <p/>
+ * 2. Maintain a list of service-side channels registered by the application.
+ * <p/>
+ * 3. Bind emulator connection with service-side channel via port name, provided by
+ * the emulator.
+ * <p/>
+ * 4. Monitor connection state with the emulator, and automatically restore the
+ * connection once it is lost.
+ */
+public class Connection {
+ /** UNIX-domain name reserved for SDK controller. */
+ public static final String SDK_CONTROLLER_PORT = "android.sdk.controller";
+ /** Tag for logging messages. */
+ private static final String TAG = "SdkControllerConnection";
+ /** Controls debug logging */
+ private static final boolean DEBUG = false;
+
+ /** Server socket used to listen to emulator connections. */
+ private LocalServerSocket mServerSocket = null;
+ /** Service that has created this object. */
+ private ControllerService mService;
+ /**
+ * List of connected emulator sockets, pending for a channel to be registered.
+ * <p/>
+ * Emulator may connect to SDK controller before the app registers a channel
+ * for that connection. In this case (when app-side channel is not registered
+ * with this class) we will keep emulator connection in this list, pending
+ * for the app-side channel to register.
+ */
+ private List<Socket> mPendingSockets = new ArrayList<Socket>();
+ /**
+ * List of registered app-side channels.
+ * <p/>
+ * Channels that are kept in this list may be disconnected from (or pending
+ * connection with) the emulator, or they may be connected with the
+ * emulator.
+ */
+ private List<Channel> mChannels = new ArrayList<Channel>();
+
+ /**
+ * Constructs Connection instance.
+ */
+ public Connection(ControllerService service) {
+ mService = service;
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is constructed.");
+ }
+
+ /**
+ * Binds to the socket, and starts the listening thread.
+ */
+ public void connect() {
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is connecting...");
+ // Start connection listener.
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ runIOLooper();
+ }
+ }, "SdkControllerConnectionIoLoop").start();
+ }
+
+ /**
+ * Stops the listener, and closes the socket.
+ *
+ * @return true if connection has been stopped in this call, or false if it
+ * has been already stopped when this method has been called.
+ */
+ public boolean disconnect() {
+ // This is the only place in this class where we will null the
+ // socket object. Since this method can be called concurrently from
+ // different threads, lets do this under the lock.
+ LocalServerSocket socket;
+ synchronized (this) {
+ socket = mServerSocket;
+ mServerSocket = null;
+ }
+ if (socket != null) {
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is stopping I/O looper...");
+ // Stop accepting new connections.
+ wakeIOLooper(socket);
+ try {
+ socket.close();
+ } catch (Exception e) {
+ }
+
+ // Close all the pending sockets, and clear pending socket list.
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is closing pending sockets...");
+ for (Socket pending_socket : mPendingSockets) {
+ pending_socket.close();
+ }
+ mPendingSockets.clear();
+
+ // Disconnect all the emualtors.
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is disconnecting channels...");
+ for (Channel channel : mChannels) {
+ if (channel.disconnect()) {
+ channel.onEmulatorDisconnected();
+ }
+ }
+ if (DEBUG) Log.d(TAG, "SdkControllerConnection is disconnected.");
+ }
+ return socket != null;
+ }
+
+ /**
+ * Registers SDK controller channel.
+ *
+ * @param channel SDK controller emulator to register.
+ * @return true if channel has been registered successfully, or false if channel
+ * with the same name is already registered.
+ */
+ public boolean registerChannel(Channel channel) {
+ for (Channel check_channel : mChannels) {
+ if (check_channel.getChannelName().equals(channel.getChannelName())) {
+ Loge("Registering a duplicate Channel " + channel.getChannelName());
+ return false;
+ }
+ }
+ if (DEBUG) Log.d(TAG, "Registering Channel " + channel.getChannelName());
+ mChannels.add(channel);
+
+ // Lets see if there is a pending socket for this channel.
+ for (Socket pending_socket : mPendingSockets) {
+ if (pending_socket.getChannelName().equals(channel.getChannelName())) {
+ // Remove the socket from the pending list, and connect the registered channel with it.
+ if (DEBUG) Log.d(TAG, "Found pending Socket for registering Channel "
+ + channel.getChannelName());
+ mPendingSockets.remove(pending_socket);
+ channel.connect(pending_socket);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks if at least one socket connection exists with channel.
+ *
+ * @return true if at least one socket connection exists with channel.
+ */
+ public boolean isEmulatorConnected() {
+ for (Channel channel : mChannels) {
+ if (channel.isConnected()) {
+ return true;
+ }
+ }
+ return !mPendingSockets.isEmpty();
+ }
+
+ /**
+ * Gets Channel instance for the given channel name.
+ *
+ * @param name Channel name to get Channel instance for.
+ * @return Channel instance for the given channel name, or NULL if no
+ * channel has been registered for that name.
+ */
+ public Channel getChannel(String name) {
+ for (Channel channel : mChannels) {
+ if (channel.getChannelName().equals(name)) {
+ return channel;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets connected emulator socket that is pending for service-side channel
+ * registration.
+ *
+ * @param name Channel name to lookup Socket for.
+ * @return Connected emulator socket that is pending for service-side channel
+ * registration, or null if no socket is pending for service-size
+ * channel registration.
+ */
+ private Socket getPendingSocket(String name) {
+ for (Socket socket : mPendingSockets) {
+ if (socket.getChannelName().equals(name)) {
+ return socket;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Wakes I/O looper waiting on connection with the emulator.
+ *
+ * @param socket Server socket waiting on connection.
+ */
+ private void wakeIOLooper(LocalServerSocket socket) {
+ // We wake the looper by connecting to the socket.
+ LocalSocket waker = new LocalSocket();
+ try {
+ waker.connect(socket.getLocalSocketAddress());
+ } catch (IOException e) {
+ Loge("Exception " + e + " in SdkControllerConnection while waking up the I/O looper.");
+ }
+ }
+
+ /**
+ * Loops on the local socket, handling emulator connection attempts.
+ */
+ private void runIOLooper() {
+ if (DEBUG) Log.d(TAG, "In SdkControllerConnection I/O looper.");
+ do {
+ try {
+ // Create non-blocking server socket that would listen for connections,
+ // and bind it to the given port on the local host.
+ mServerSocket = new LocalServerSocket(SDK_CONTROLLER_PORT);
+ LocalServerSocket socket = mServerSocket;
+ while (socket != null) {
+ final LocalSocket sk = socket.accept();
+ if (mServerSocket != null) {
+ onAccept(sk);
+ } else {
+ break;
+ }
+ socket = mServerSocket;
+ }
+ } catch (IOException e) {
+ Loge("Exception " + e + "SdkControllerConnection I/O looper.");
+ }
+ if (DEBUG) Log.d(TAG, "Exiting SdkControllerConnection I/O looper.");
+
+ // If we're exiting the internal loop for reasons other than an explicit
+ // disconnect request, we should reconnect again.
+ } while (disconnect());
+ }
+
+ /**
+ * Accepts new connection from the emulator.
+ *
+ * @param sock Connecting socket.
+ * @throws IOException
+ */
+ private void onAccept(LocalSocket sock) throws IOException {
+ final ByteBuffer handshake = ByteBuffer.allocate(ProtocolConstants.QUERY_HEADER_SIZE);
+
+ // By protocol, first byte received from newly connected emulator socket
+ // indicates host endianness.
+ Socket.receive(sock, handshake.array(), 1);
+ final ByteOrder endian = (handshake.getChar() == 0) ? ByteOrder.LITTLE_ENDIAN :
+ ByteOrder.BIG_ENDIAN;
+ handshake.order(endian);
+
+ // Right after that follows the handshake query header.
+ handshake.position(0);
+ Socket.receive(sock, handshake.array(), handshake.array().length);
+
+ // First int - signature
+ final int signature = handshake.getInt();
+ assert signature == ProtocolConstants.PACKET_SIGNATURE;
+ // Second int - total query size (including fixed query header)
+ final int remains = handshake.getInt() - ProtocolConstants.QUERY_HEADER_SIZE;
+ // After that - header type (which must be SDKCTL_PACKET_TYPE_QUERY)
+ final int msg_type = handshake.getInt();
+ assert msg_type == ProtocolConstants.PACKET_TYPE_QUERY;
+ // After that - query ID.
+ final int query_id = handshake.getInt();
+ // And finally, query type (which must be ProtocolConstants.QUERY_HANDSHAKE for
+ // handshake query)
+ final int query_type = handshake.getInt();
+ assert query_type == ProtocolConstants.QUERY_HANDSHAKE;
+ // Verify that received is a query.
+ if (msg_type != ProtocolConstants.PACKET_TYPE_QUERY) {
+ // Message type is not a query. Lets read and discard the remainder
+ // of the message.
+ if (remains > 0) {
+ Loge("Unexpected handshake message type: " + msg_type);
+ byte[] discard = new byte[remains];
+ Socket.receive(sock, discard, discard.length);
+ }
+ return;
+ }
+
+ // Receive query data.
+ final byte[] name_array = new byte[remains];
+ Socket.receive(sock, name_array, name_array.length);
+
+ // Prepare response header.
+ handshake.position(0);
+ handshake.putInt(ProtocolConstants.PACKET_SIGNATURE);
+ // Handshake reply is just one int.
+ handshake.putInt(ProtocolConstants.QUERY_RESP_HEADER_SIZE + 4);
+ handshake.putInt(ProtocolConstants.PACKET_TYPE_QUERY_RESPONSE);
+ handshake.putInt(query_id);
+
+ // Verify that received query is in deed a handshake query.
+ if (query_type != ProtocolConstants.QUERY_HANDSHAKE) {
+ // Query is not a handshake. Reply with failure.
+ Loge("Unexpected handshake query type: " + query_type);
+ handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_QUERY_UNKNOWN);
+ sock.getOutputStream().write(handshake.array());
+ return;
+ }
+
+ // Handshake query data consist of SDK controller channel name.
+ final String channel_name = new String(name_array);
+ if (DEBUG) Log.d(TAG, "Handshake received for channel " + channel_name);
+
+ // Respond to query depending on service-side channel availability
+ final Channel channel = getChannel(channel_name);
+ Socket sk = null;
+
+ if (channel != null) {
+ if (channel.isConnected()) {
+ // This is a duplicate connection.
+ Loge("Duplicate connection to a connected Channel " + channel_name);
+ handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_DUP);
+ } else {
+ // Connecting to a registered channel.
+ if (DEBUG) Log.d(TAG, "Emulator is connected to a registered Channel " + channel_name);
+ handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_CONNECTED);
+ }
+ } else {
+ // Make sure that there are no other channel connections for this
+ // channel name.
+ if (getPendingSocket(channel_name) != null) {
+ // This is a duplicate.
+ Loge("Duplicate connection to a pending Socket " + channel_name);
+ handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_DUP);
+ } else {
+ // Connecting to a channel that has not been registered yet.
+ if (DEBUG) Log.d(TAG, "Emulator is connected to a pending Socket " + channel_name);
+ handshake.putInt(ProtocolConstants.HANDSHAKE_RESP_NOPORT);
+ sk = new Socket(sock, channel_name, endian);
+ mPendingSockets.add(sk);
+ }
+ }
+
+ // Send handshake reply.
+ sock.getOutputStream().write(handshake.array());
+
+ // If a disconnected channel for emulator connection has been found,
+ // connect it.
+ if (channel != null && !channel.isConnected()) {
+ if (DEBUG) Log.d(TAG, "Connecting Channel " + channel_name + " with emulator.");
+ sk = new Socket(sock, channel_name, endian);
+ channel.connect(sk);
+ }
+
+ mService.notifyStatusChanged();
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ mService.addError(log);
+ Log.e(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java
new file mode 100644
index 000000000..32abf2bc0
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/ProtocolConstants.java
@@ -0,0 +1,146 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.android.tools.sdkcontroller.lib;
+
+/**
+ * Contains declarations of constants that are tied to emulator implementation.
+ * These constants can be changed only simultaneously in both places.
+ */
+public final class ProtocolConstants {
+ /*
+ * Constants related to data transfer.
+ */
+
+ /** Signature of a packet sent via SDK controller socket ('SDKC') */
+ public static final int PACKET_SIGNATURE = 0x53444B43;
+
+ /*
+ * Header sizes for packets sent / received by SDK controller emulator.
+ */
+
+ /**
+ * 12 bytes (3 ints) for the packet header:
+ * <p/>
+ * - Signature.
+ * <p/>
+ * - Total packet size.
+ * <p/>
+ * - Packet type.
+ */
+ public static final int PACKET_HEADER_SIZE = 12;
+ /**
+ * 16 bytes (4 ints) for the message header:
+ * <p/>
+ * - Common packet header.
+ * <p/>
+ * - Message type.
+ */
+ public static final int MESSAGE_HEADER_SIZE = 16;
+ /**
+ * 20 bytes (5 ints) for the query header:
+ * <p/>
+ * - Common packet header.
+ * <p/>
+ * - Query ID.
+ * <p/>
+ * - Query type.
+ */
+ public static final int QUERY_HEADER_SIZE = 20;
+ /**
+ * 16 bytes (4 ints) for the query response:
+ * <p/>
+ * - Common packet header.
+ * <p/>
+ * - Query ID.
+ */
+ public static final int QUERY_RESP_HEADER_SIZE = 16;
+
+ /*
+ * Types of packets transferred via SDK Controller channel.
+ */
+
+ /** Packet is a message. */
+ public static final int PACKET_TYPE_MESSAGE = 1;
+ /** Packet is a query. */
+ public static final int PACKET_TYPE_QUERY = 2;
+ /** Packet is a response to a query. */
+ public static final int PACKET_TYPE_QUERY_RESPONSE = 3;
+
+ /*
+ * Constants related to handshake protocol between the emulator and a channel.
+ */
+
+ /**
+ * Query type for a special "handshake" query.
+ * <p/>
+ * When emulator connects to SDK controller, the first thing that goes
+ * through the socket is a special "handshake" query that delivers channel name
+ * to the service.
+ */
+ public static final int QUERY_HANDSHAKE = -1;
+ /**
+ * Handshake query response on condition that service-side channel is available
+ * (registered).
+ */
+ public static final int HANDSHAKE_RESP_CONNECTED = 0;
+ /**
+ * Handshake query response on condition that service-side channel is not
+ * available (not registered).
+ */
+ public static final int HANDSHAKE_RESP_NOPORT = 1;
+ /**
+ * Handshake query response on condition that there is already an existing
+ * emulator connection for this channel. Emulator should stop connection
+ * attempts in this case.
+ */
+ public static final int HANDSHAKE_RESP_DUP = -1;
+ /** Response to an unknown handshake query type. */
+ public static final int HANDSHAKE_RESP_QUERY_UNKNOWN = -2;
+
+ /*
+ * Constants related to multi-touch emulation.
+ */
+
+ /** Received frame is JPEG image. */
+ public static final int MT_FRAME_JPEG = 1;
+ /** Received frame is RGB565 bitmap. */
+ public static final int MT_FRAME_RGB565 = 2;
+ /** Received frame is RGB888 bitmap. */
+ public static final int MT_FRAME_RGB888 = 3;
+
+ /** Pointer(s) moved. */
+ public static final int MT_MOVE = 1;
+ /** First pointer down message. */
+ public static final int MT_FISRT_DOWN = 2;
+ /** Last pointer up message. */
+ public static final int MT_LAST_UP = 3;
+ /** Pointer down message. */
+ public static final int MT_POINTER_DOWN = 4;
+ /** Pointer up message. */
+ public static final int MT_POINTER_UP = 5;
+ /** Sends framebuffer update. */
+ public static final int MT_FB_UPDATE = 6;
+ /** Frame buffer update has been received. */
+ public static final int MT_FB_ACK = 7;
+ /** Frame buffer update has been handled. */
+ public static final int MT_FB_HANDLED = 8;
+ /** Size of an event entry in the touch event message to the emulator. */
+ public static final int MT_EVENT_ENTRY_SIZE = 16;
+
+ /*
+ * Constants related to sensor emulation.
+ */
+
+ /** Query type for a query that should return the list of available sensors. */
+ public static final int SENSORS_QUERY_LIST = 1;
+ /** Message that starts sensor emulation. */
+ public static final int SENSORS_START = 1;
+ /** Message that stops sensor emulation. */
+ public static final int SENSORS_STOP = 2;
+ /** Message that enables emulation of a particular sensor. */
+ public static final int SENSORS_ENABLE = 3;
+ /** Message that disables emulation of a particular sensor. */
+ public static final int SENSORS_DISABLE = 4;
+ /** Message that delivers sensor events to emulator. */
+ public static final int SENSORS_SENSOR_EVENT = 5;
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java
new file mode 100644
index 000000000..08e6b2813
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/lib/Socket.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.lib;
+
+import android.net.LocalSocket;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.channels.ClosedChannelException;
+
+/**
+ * Encapsulates a connection with the emulator over a UNIX-domain socket.
+ */
+public class Socket {
+ /** UNIX-domain socket connected with the emulator. */
+ private LocalSocket mSocket = null;
+ /** Channel name for the connection established via this socket. */
+ private String mChannelName;
+ /** Endianness of data transferred in this connection. */
+ private ByteOrder mEndian;
+
+ /** Tag for message logging. */
+ private static final String TAG = "SdkControllerSocket";
+ /** Controls debug log. */
+ private static boolean DEBUG = false;
+
+ /**
+ * Constructs Socket instance.
+ *
+ * @param socket Socket connection with the emulator.
+ * @param name Channel port name for this connection.
+ * @param endian Endianness of data transferred in this connection.
+ */
+ public Socket(LocalSocket socket, String name, ByteOrder endian) {
+ mSocket = socket;
+ mChannelName = name;
+ mEndian = endian;
+ if (DEBUG) Log.d(TAG, "Socket is constructed for " + mChannelName);
+ }
+
+ /**
+ * Gets connection status of this socket.
+ *
+ * @return true if socket is connected, or false if socket is not connected.
+ */
+ public boolean isConnected() {
+ return mSocket != null;
+ }
+
+ /**
+ * Gets channel name for this socket.
+ *
+ * @return Channel name for this socket.
+ */
+ public String getChannelName() {
+ return mChannelName;
+ }
+
+ /**
+ * Gets endianness of data transferred via this socket.
+ *
+ * @return Endianness of data transferred via this socket.
+ */
+ public ByteOrder getEndian() {
+ return mEndian;
+ }
+
+ /**
+ * Sends data to the socket.
+ *
+ * @param data Data to send. Data size is defined by the length of the
+ * array.
+ * @throws IOException
+ */
+ public void send(byte[] data) throws IOException {
+ // Use local copy of the socket, ensuring it's not going to NULL while
+ // we're working with it. If it gets closed, while we're in the middle
+ // of data transfer - it's OK, since it will produce an exception, and
+ // the caller will gracefully handle it.
+ //
+ // Same technique is used everywhere in this class where mSocket member
+ // is touched.
+ LocalSocket socket = mSocket;
+ if (socket == null) {
+ Logw("'send' request on closed Socket " + mChannelName);
+ throw new ClosedChannelException();
+ }
+ socket.getOutputStream().write(data);
+ }
+
+ /**
+ * Sends data to the socket.
+ *
+ * @param data Data to send.
+ * @param offset The start position in data from where to get bytes.
+ * @param len The number of bytes from data to write to this socket.
+ * @throws IOException
+ */
+ public void send(byte[] data, int offset, int len) throws IOException {
+ LocalSocket socket = mSocket;
+ if (socket == null) {
+ Logw("'send' request on closed Socket " + mChannelName);
+ throw new ClosedChannelException();
+ }
+ socket.getOutputStream().write(data, offset, len);
+ }
+
+ /**
+ * Receives data from the socket.
+ *
+ * @param socket Socket from where to receive data.
+ * @param data Array where to save received data.
+ * @param len Number of bytes to receive.
+ * @throws IOException
+ */
+ public static void receive(LocalSocket socket, byte[] data, int len) throws IOException {
+ final InputStream is = socket.getInputStream();
+ int received = 0;
+ while (received != len) {
+ final int chunk = is.read(data, received, len - received);
+ if (chunk < 0) {
+ throw new IOException(
+ "I/O failure while receiving SDK controller data from socket.");
+ }
+ received += chunk;
+ }
+ }
+
+ /**
+ * Receives data from the socket.
+ *
+ * @param data Array where to save received data.
+ * @param len Number of bytes to receive.
+ * @throws IOException
+ */
+ public void receive(byte[] data, int len) throws IOException {
+ LocalSocket socket = mSocket;
+ if (socket == null) {
+ Logw("'receive' request on closed Socket " + mChannelName);
+ throw new ClosedChannelException();
+ }
+ receive(socket, data, len);
+ }
+
+ /**
+ * Receives data from the socket.
+ *
+ * @param data Array where to save received data. Data size is defined by
+ * the size of the array.
+ * @throws IOException
+ */
+ public void receive(byte[] data) throws IOException {
+ receive(data, data.length);
+ }
+
+ /**
+ * Closes the socket.
+ *
+ * @return true if socket has been closed in this call, or false if it had
+ * been already closed when this method has been called.
+ */
+ public boolean close() {
+ // This is the only place in this class where we will null the socket
+ // object. Since this method can be called concurrently from different
+ // threads, lets do this under the lock.
+ LocalSocket socket;
+ synchronized (this) {
+ socket = mSocket;
+ mSocket = null;
+ }
+ if (socket != null) {
+ try {
+ // Force all I/O to stop before closing the socket.
+ socket.shutdownInput();
+ socket.shutdownOutput();
+ socket.close();
+ if (DEBUG) Log.d(TAG, "Socket is closed for " + mChannelName);
+ return true;
+ } catch (IOException e) {
+ Loge("Exception " + e + " while closing Socket for " + mChannelName);
+ }
+ }
+ return false;
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ private void Loge(String log) {
+ Log.e(TAG, log);
+ }
+
+ private void Logw(String log) {
+ Log.w(TAG, log);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java b/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java
new file mode 100755
index 000000000..9a3408b3e
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/service/ControllerService.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.tools.sdkcontroller.R;
+import com.android.tools.sdkcontroller.activities.MainActivity;
+import com.android.tools.sdkcontroller.handlers.MultiTouchChannel;
+import com.android.tools.sdkcontroller.handlers.SensorChannel;
+import com.android.tools.sdkcontroller.lib.Connection;
+import com.android.tools.sdkcontroller.lib.Channel;
+
+/**
+ * The background service of the SdkController.
+ * There can be only one instance of this.
+ * <p/>
+ * The service manages a number of SDK controller channels which can be seen as
+ * individual tasks that the user might want to accomplish, for example "sending
+ * sensor data to the emulator" or "sending multi-touch data and displaying an
+ * emulator screen".
+ * <p/>
+ * Each channel connects to the emulator via UNIX-domain socket that is bound to
+ * "android.sdk.controller" port. Connection class provides a socket server that
+ * listens to emulator connections on this port, and binds new connection with a
+ * channel, based on channel name.
+ * <p/>
+ * All the channels are created when the service starts, and whether the emulator
+ * connection is successful or not, and whether there's any UI to control it.
+ * It's up to the channels to deal with these specific details. <br/>
+ * For example the {@link SensorChannel} initializes its sensor list as soon as
+ * created and then tries to send data as soon as there's an emulator
+ * connection. On the other hand the {@link MultiTouchChannel} lays dormant till
+ * there's an UI interacting with it.
+ */
+public class ControllerService extends Service {
+
+ /*
+ * Implementation reference:
+ * http://developer.android.com/reference/android/app/Service.html#LocalServiceSample
+ */
+
+ /** Tag for logging messages. */
+ public static String TAG = ControllerService.class.getSimpleName();
+ /** Controls debug log. */
+ private static boolean DEBUG = true;
+ /** Identifier for the notification. */
+ private static int NOTIF_ID = 'S' << 24 + 'd' << 16 + 'k' << 8 + 'C' << 0;
+
+ /** Connection to the emulator. */
+ public Connection mConnection;
+
+
+ private final IBinder mBinder = new ControllerBinder();
+
+ private List<ControllerListener> mListeners = new ArrayList<ControllerListener>();
+
+ /**
+ * Whether the service is running. Set to true in onCreate, false in onDestroy.
+ */
+ private static volatile boolean gServiceIsRunning = false;
+
+ /** Internal error reported by the service. */
+ private String mServiceError = "";
+
+ /**
+ * Interface that the service uses to notify binded activities.
+ * <p/>
+ * As a design rule, implementations of this listener should be aware that most calls
+ * will NOT happen on the UI thread. Any access to the UI should be properly protected
+ * by using {@link Activity#runOnUiThread(Runnable)}.
+ */
+ public interface ControllerListener {
+ /**
+ * The error string reported by the service has changed. <br/>
+ * Note this may be called from a thread different than the UI thread.
+ */
+ void onErrorChanged();
+
+ /**
+ * The service status has changed (emulator connected/disconnected.)
+ */
+ void onStatusChanged();
+ }
+
+ /** Interface that callers can use to access the service. */
+ public class ControllerBinder extends Binder {
+
+ /**
+ * Adds a new listener that will be notified when the service state changes.
+ *
+ * @param listener A non-null listener. Ignored if already listed.
+ */
+ public void addControllerListener(ControllerListener listener) {
+ assert listener != null;
+ if (listener != null) {
+ synchronized (mListeners) {
+ if (!mListeners.contains(listener)) {
+ mListeners.add(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a listener.
+ *
+ * @param listener A listener to remove. Can be null.
+ */
+ public void removeControllerListener(ControllerListener listener) {
+ assert listener != null;
+ synchronized (mListeners) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Returns the error string accumulated by the service.
+ * Typically these would relate to failures to establish the communication
+ * channel(s) with the emulator, or unexpected disconnections.
+ */
+ public String getServiceError() {
+ return mServiceError;
+ }
+
+ /**
+ * Indicates when <em>any</all> of the SDK controller channels is connected
+ * with the emulator.
+ *
+ * @return True if any of the SDK controller channels is connected with the
+ * emulator.
+ */
+ public boolean isEmuConnected() {
+ return mConnection.isEmulatorConnected();
+ }
+
+ /**
+ * Returns the channel instance for the given type.
+ *
+ * @param name One of the channel names. Must not be null.
+ * @return Null if the type is not found, otherwise the handler's unique instance.
+ */
+ public Channel getChannel(String name) {
+ return mConnection.getChannel(name);
+ }
+ }
+
+ /**
+ * Whether the service is running. Set to true in onCreate, false in onDestroy.
+ */
+ public static boolean isServiceIsRunning() {
+ return gServiceIsRunning;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ if (DEBUG) Log.d(TAG, "Service onCreate");
+ gServiceIsRunning = true;
+ showNotification();
+ onServiceStarted();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // We want this service to continue running until it is explicitly
+ // stopped, so return sticky.
+ if (DEBUG) Log.d(TAG, "Service onStartCommand");
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (DEBUG) Log.d(TAG, "Service onBind");
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (DEBUG) Log.d(TAG, "Service onDestroy");
+ gServiceIsRunning = false;
+ removeNotification();
+ resetError();
+ onServiceStopped();
+ super.onDestroy();
+ }
+
+ private void disconnectAll() {
+ if (mConnection != null) {
+ mConnection.disconnect();
+ }
+ }
+
+ /**
+ * Called when the service has been created.
+ */
+ private void onServiceStarted() {
+ try {
+ disconnectAll();
+
+ // Bind to SDK controller port, and start accepting emulator
+ // connections.
+ mConnection = new Connection(ControllerService.this);
+ mConnection.connect();
+
+ // Create and register sensors channel.
+ mConnection.registerChannel(new SensorChannel(ControllerService.this));
+ // Create and register multi-touch channel.
+ mConnection.registerChannel(new MultiTouchChannel(ControllerService.this));
+ } catch (Exception e) {
+ addError("Connection failed: " + e.toString());
+ }
+ }
+
+ /**
+ * Called when the service is being destroyed.
+ */
+ private void onServiceStopped() {
+ disconnectAll();
+ }
+
+ private void notifyErrorChanged() {
+ synchronized (mListeners) {
+ for (ControllerListener listener : mListeners) {
+ listener.onErrorChanged();
+ }
+ }
+ }
+
+ public void notifyStatusChanged() {
+ synchronized (mListeners) {
+ for (ControllerListener listener : mListeners) {
+ listener.onStatusChanged();
+ }
+ }
+ }
+
+ /**
+ * Resets the error string and notify listeners.
+ */
+ private void resetError() {
+ mServiceError = "";
+
+ notifyErrorChanged();
+ }
+
+ /**
+ * An internal utility method to add a line to the error string and notify listeners.
+ * @param error A non-null non-empty error line. \n will be added automatically.
+ */
+ public void addError(String error) {
+ Log.e(TAG, error);
+ if (mServiceError.length() > 0) {
+ mServiceError += "\n";
+ }
+ mServiceError += error;
+
+ notifyErrorChanged();
+ }
+
+ /**
+ * Displays a notification showing that the service is running.
+ * When the user touches the notification, it opens the main activity
+ * which allows the user to stop this service.
+ */
+ @SuppressWarnings("deprecated")
+ private void showNotification() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ String text = getString(R.string.service_notif_title);
+
+ // Note: Notification is marked as deprecated -- in API 11+ there's a new Builder class
+ // but we need to have API 7 compatibility so we ignore that warning.
+
+ Notification n = new Notification(R.drawable.ic_launcher, text, System.currentTimeMillis());
+ n.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ PendingIntent pi = PendingIntent.getActivity(
+ this, //context
+ 0, //requestCode
+ intent, //intent
+ 0 //pending intent flags
+ );
+ n.setLatestEventInfo(this, text, text, pi);
+
+ nm.notify(NOTIF_ID, n);
+ }
+
+ private void removeNotification() {
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ nm.cancel(NOTIF_ID);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java
new file mode 100755
index 000000000..66bce49f3
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.utils;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.view.View;
+
+/**
+ * Helper to deal with methods only available at certain API levels.
+ * Users should get use {@link ApiHelper#get()} to retrieve a singleton
+ * and then call the methods they desire. If the method is not available
+ * on the current API level, a stub or a nop will be used instead.
+ */
+@TargetApi(7)
+public class ApiHelper {
+
+ private static ApiHelper sApiHelper = null;
+
+ /** Creates a new ApiHelper adapted to the current runtime API level. */
+ public static ApiHelper get() {
+ if (sApiHelper == null) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ sApiHelper = new ApiHelper_11();
+ } else {
+ sApiHelper = new ApiHelper();
+ }
+ }
+
+ return sApiHelper;
+ }
+
+ protected ApiHelper() {
+ }
+
+ /**
+ * Applies {@link View#setSystemUiVisibility(int)}, available only starting with API 11.
+ * Does nothing for API < 11.
+ */
+ public void View_setSystemUiVisibility(View view, int visibility) {
+ // nop
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java
new file mode 100755
index 000000000..2d4e8cd3f
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/utils/ApiHelper_11.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.utils;
+
+import android.annotation.TargetApi;
+import android.view.View;
+
+/**
+ * API 11: support View_setSystemUiVisibility
+ */
+@TargetApi(11)
+class ApiHelper_11 extends ApiHelper {
+
+ /**
+ * Applies {@link View#setSystemUiVisibility(int)}, available only starting with API 11.
+ * Does nothing for API < 11.
+ */
+ @Override
+ public void View_setSystemUiVisibility(View view, int visibility) {
+ view.setSystemUiVisibility(visibility);
+ }
+}
diff --git a/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java b/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java
new file mode 100755
index 000000000..0f185b173
--- /dev/null
+++ b/apps/SdkController/src/com/android/tools/sdkcontroller/views/MultiTouchView.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2012 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.sdkcontroller.views;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Implements a main view for the application providing multi-touch emulation.
+ */
+public class MultiTouchView extends View {
+ /** Tag for logging messages. */
+ private static final String TAG = MultiTouchView.class.getSimpleName();
+ /**
+ * Back-end bitmap. Initialized in onSizeChanged(), updated in
+ * onTouchEvent() and drawn in onDraw().
+ */
+ private Bitmap mBitmap;
+ /** Default Paint instance for drawing the bitmap. */
+ private final Paint mPaint = new Paint();
+ /** Canvas instance for this view. */
+ private Canvas mCanvas;
+ /** Emulator screen width to this view width ratio. */
+ private float mDx = 1;
+ /** Emulator screen height to this view height ratio. */
+ private float mDy = 1;
+ /**
+ * Flags whether or not image received from the emulator should be rotated.
+ * Rotation is required when display orientation state of the emulator and
+ * the device doesn't match.
+ */
+ private boolean mRotateDisplay;
+ /** Base matrix that keep emulator->device display scaling */
+ private Matrix mBaseMatrix = new Matrix();
+ /** Matrix that is used to draw emulator's screen on the device. */
+ private Matrix mDrawMatrix = new Matrix();
+
+ /**
+ * Simple constructor to use when creating a view from code.
+ *
+ * @see View#View(Context)
+ */
+ public MultiTouchView(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Constructor that is called when inflating a view from XML.
+ *
+ * @see View#View(Context, AttributeSet)
+ */
+ public MultiTouchView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ /**
+ * Perform inflation from XML and apply a class-specific base style.
+ *
+ * @see View#View(Context, AttributeSet, int)
+ */
+ public MultiTouchView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ // TODO Add constructor-time code here.
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas(mBitmap);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ // Just draw the back-end bitmap without zooming or scaling.
+ if (mBitmap != null) {
+ canvas.drawBitmap(mBitmap, 0, 0, null);
+ }
+ }
+
+ /**
+ * Sets emulator screen width and height to this view width and height
+ * ratio.
+ *
+ * @param dx Emulator screen width to this view width ratio.
+ * @param dy Emulator screen height to this view height ratio.
+ * @param rotateDisplay Flags whether image received from the emulator
+ * should be rotated when drawn on the device.
+ */
+ public void setDxDy(float dx, float dy, boolean rotateDisplay) {
+ mDx = dx;
+ mDy = dy;
+ mRotateDisplay = rotateDisplay;
+
+ mBaseMatrix.setScale(dx, dy);
+ if (mRotateDisplay) {
+ mBaseMatrix.postRotate(90);
+ mBaseMatrix.postTranslate(getWidth(), 0);
+ }
+ }
+
+ /**
+ * Computes draw matrix for the emulator screen update.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ */
+ private void computeDrawMatrix(int x, int y) {
+ mDrawMatrix.set(mBaseMatrix);
+ if (mRotateDisplay) {
+ mDrawMatrix.postTranslate(-y * mDy, x * mDx);
+ } else {
+ mDrawMatrix.postTranslate(x * mDx, y * mDy);
+ }
+ }
+
+ /**
+ * Draws a bitmap on the screen.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ * @param w Width of the bitmap on the emulator screen.
+ * @param h Height of the bitmap on the emulator screen.
+ * @param colors Bitmap to draw.
+ */
+ public void drawBitmap(int x, int y, int w, int h, int[] colors) {
+ if (mCanvas != null) {
+ final Bitmap bmp = Bitmap.createBitmap(colors, 0, w, w, h, Bitmap.Config.ARGB_8888);
+
+ computeDrawMatrix(x, y);
+
+ /* Draw the bitmap and invalidate the updated region. */
+ mCanvas.drawBitmap(bmp, mDrawMatrix, mPaint);
+ invalidate();
+ }
+ }
+
+ /**
+ * Draws a JPEG bitmap on the screen.
+ *
+ * @param x Left screen coordinate of the bitmap on emulator screen.
+ * @param y Top screen coordinate of the bitmap on emulator screen.
+ * @param w Width of the bitmap on the emulator screen.
+ * @param h Height of the bitmap on the emulator screen.
+ * @param jpeg JPEG bitmap to draw.
+ */
+ public void drawJpeg(int x, int y, int w, int h, InputStream jpeg) {
+ if (mCanvas != null) {
+ final Bitmap bmp = BitmapFactory.decodeStream(jpeg);
+
+ computeDrawMatrix(x, y);
+
+ /* Draw the bitmap and invalidate the updated region. */
+ mCanvas.drawBitmap(bmp, mDrawMatrix, mPaint);
+ invalidate();
+ }
+ }
+
+ /**
+ * Constructs touch event message to be send to emulator.
+ *
+ * @param bb ByteBuffer where to construct the message.
+ * @param event Event for which to construct the message.
+ * @param ptr_index Index of the motion pointer for which to construct the
+ * message.
+ */
+ public void constructEventMessage(ByteBuffer bb, MotionEvent event, int ptr_index) {
+ bb.putInt(event.getPointerId(ptr_index));
+ if (mRotateDisplay == false) {
+ bb.putInt((int) (event.getX(ptr_index) / mDx));
+ bb.putInt((int) (event.getY(ptr_index) / mDy));
+ } else {
+ bb.putInt((int) (event.getY(ptr_index) / mDy));
+ bb.putInt((int) (getWidth() - event.getX(ptr_index) / mDx));
+ }
+ // At the system level the input reader takes integers in the range
+ // 0 - 100 for the pressure.
+ int pressure = (int) (event.getPressure(ptr_index) * 100);
+ // Make sure it doesn't exceed 100...
+ if (pressure > 100) {
+ pressure = 100;
+ }
+ bb.putInt(pressure);
+ }
+
+ /***************************************************************************
+ * Logging wrappers
+ **************************************************************************/
+
+ @SuppressWarnings("unused")
+ private void Loge(String log) {
+ Log.e(TAG, log);
+ }
+
+ @SuppressWarnings("unused")
+ private void Logw(String log) {
+ Log.w(TAG, log);
+ }
+
+ @SuppressWarnings("unused")
+ private void Logv(String log) {
+ Log.v(TAG, log);
+ }
+}