diff options
Diffstat (limited to 'apps')
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 Binary files differnew file mode 100644 index 000000000..f92dea2aa --- /dev/null +++ b/apps/NotificationStudio/libs/android-support-v4.jar 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 Binary files differnew file mode 100644 index 000000000..6078e8632 --- /dev/null +++ b/apps/NotificationStudio/res/drawable-hdpi/android_logo.gif 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 Binary files differnew file mode 100644 index 000000000..c4f3648c2 --- /dev/null +++ b/apps/NotificationStudio/res/drawable-hdpi/ic_notification_multiple_mail_holo_dark.png diff --git a/apps/NotificationStudio/res/drawable-hdpi/romain.jpg b/apps/NotificationStudio/res/drawable-hdpi/romain.jpg Binary files differnew file mode 100644 index 000000000..e93290199 --- /dev/null +++ b/apps/NotificationStudio/res/drawable-hdpi/romain.jpg 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 Binary files differnew file mode 100644 index 000000000..68473ba6c --- /dev/null +++ b/apps/NotificationStudio/res/drawable-nodpi/romainguy_rockaway.jpg 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 > 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 Binary files differnew file mode 100755 index 000000000..f8c12940f --- /dev/null +++ b/apps/SdkController/bin/SdkControllerApp.apk 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 Binary files differnew file mode 100755 index 000000000..96a442e5b --- /dev/null +++ b/apps/SdkController/res/drawable-hdpi/ic_launcher.png diff --git a/apps/SdkController/res/drawable-ldpi/ic_launcher.png b/apps/SdkController/res/drawable-ldpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..99238729d --- /dev/null +++ b/apps/SdkController/res/drawable-ldpi/ic_launcher.png diff --git a/apps/SdkController/res/drawable-mdpi/ic_launcher.png b/apps/SdkController/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..359047dfa --- /dev/null +++ b/apps/SdkController/res/drawable-mdpi/ic_launcher.png diff --git a/apps/SdkController/res/drawable-xhdpi/ic_launcher.png b/apps/SdkController/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..71c6d760f --- /dev/null +++ b/apps/SdkController/res/drawable-xhdpi/ic_launcher.png 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 > 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); + } +} |