diff options
author | Alexander Lucas <alexlucas@google.com> | 2014-03-07 13:34:45 -0800 |
---|---|---|
committer | Alexander Lucas <alexlucas@google.com> | 2014-03-10 09:21:36 -0700 |
commit | a780ba4b15cbe69e7ad74c34c21ccef5e8cdce23 (patch) | |
tree | 9cf7ce9e69b696027d16f754ce40eb0640d05af0 /samples | |
parent | 7cd4524c3bc377aa4267c7014c126e0526b3c703 (diff) | |
download | android_development-a780ba4b15cbe69e7ad74c34c21ccef5e8cdce23.tar.gz android_development-a780ba4b15cbe69e7ad74c34c21ccef5e8cdce23.tar.bz2 android_development-a780ba4b15cbe69e7ad74c34c21ccef5e8cdce23.zip |
Adding browsable prebuilt samples for march push
Change-Id: I952db10d9c9acb4940db08a07789347ea2effe4d
Diffstat (limited to 'samples')
213 files changed, 15681 insertions, 93 deletions
diff --git a/samples/browseable/AdapterTransition/AndroidManifest.xml b/samples/browseable/AdapterTransition/AndroidManifest.xml new file mode 100644 index 000000000..01b414d9c --- /dev/null +++ b/samples/browseable/AdapterTransition/AndroidManifest.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.adaptertransition" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk + android:minSdkVersion="19" + android:targetSdkVersion="19"/> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme"> + <activity + android:name="com.example.android.adaptertransition.MainActivity" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/samples/browseable/AdapterTransition/_index.jd b/samples/browseable/AdapterTransition/_index.jd new file mode 100644 index 000000000..24ca6e182 --- /dev/null +++ b/samples/browseable/AdapterTransition/_index.jd @@ -0,0 +1,12 @@ + + + +page.tags="AdapterTransition" +sample.group=UI +@jd:body + +<p> + + Transition cannot be directly applied to AdapterViews. In this sample, we demonstrate how to create a overlay layout and run a Transition on it. + + </p> diff --git a/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_grid.png b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_grid.png Binary files differnew file mode 100644 index 000000000..e04f4a7f5 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_grid.png diff --git a/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_list.png b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_list.png Binary files differnew file mode 100644 index 000000000..4131dba4e --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_action_list.png diff --git a/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_launcher.png b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..b7a67c0cf --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-hdpi/ic_launcher.png diff --git a/samples/browseable/AdapterTransition/res/drawable-hdpi/tile.9.png b/samples/browseable/AdapterTransition/res/drawable-hdpi/tile.9.png Binary files differnew file mode 100644 index 000000000..135862883 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_grid.png b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_grid.png Binary files differnew file mode 100644 index 000000000..f2a83e38c --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_grid.png diff --git a/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_list.png b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_list.png Binary files differnew file mode 100644 index 000000000..e248a488e --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_action_list.png diff --git a/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_launcher.png b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..1c9fc097d --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-mdpi/ic_launcher.png diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p1.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p1.jpg Binary files differnew file mode 100644 index 000000000..10f07acf1 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p1.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p10.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p10.jpg Binary files differnew file mode 100644 index 000000000..4272f4cc0 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p10.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p11.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p11.jpg Binary files differnew file mode 100644 index 000000000..c5722b229 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p11.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p2.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p2.jpg Binary files differnew file mode 100644 index 000000000..ca380ae4e --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p2.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p3.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p3.jpg Binary files differnew file mode 100644 index 000000000..6fc71e765 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p3.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p4.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p4.jpg Binary files differnew file mode 100644 index 000000000..153c1ffe8 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p4.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p5.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p5.jpg Binary files differnew file mode 100644 index 000000000..46d6a13df --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p5.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p6.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p6.jpg Binary files differnew file mode 100644 index 000000000..89ccb8329 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p6.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p7.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p7.jpg Binary files differnew file mode 100644 index 000000000..7e9546dde --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p7.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p8.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p8.jpg Binary files differnew file mode 100644 index 000000000..21e25ba17 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p8.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-nodpi/p9.jpg b/samples/browseable/AdapterTransition/res/drawable-nodpi/p9.jpg Binary files differnew file mode 100644 index 000000000..79854cb64 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-nodpi/p9.jpg diff --git a/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_grid.png b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_grid.png Binary files differnew file mode 100644 index 000000000..ecd39b530 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_grid.png diff --git a/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_list.png b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_list.png Binary files differnew file mode 100644 index 000000000..e7e510d07 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_action_list.png diff --git a/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..11b992810 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_grid.png b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_grid.png Binary files differnew file mode 100644 index 000000000..3ba98fc15 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_grid.png diff --git a/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_list.png b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_list.png Binary files differnew file mode 100644 index 000000000..d18773288 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_action_list.png diff --git a/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..f136c9f14 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/browseable/AdapterTransition/res/layout-w720dp/activity_main.xml b/samples/browseable/AdapterTransition/res/layout-w720dp/activity_main.xml new file mode 100755 index 000000000..c9a52f621 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout-w720dp/activity_main.xml @@ -0,0 +1,73 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/sample_main_layout"> + + <LinearLayout + android:id="@+id/sample_output" + android:layout_width="0px" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <FrameLayout + style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/margin_medium" + android:paddingRight="@dimen/margin_medium" + android:paddingTop="@dimen/margin_large" + android:paddingBottom="@dimen/margin_large" + android:text="@string/intro_message" /> + </FrameLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@android:color/darker_gray" /> + + <fragment + android:name="com.example.android.common.logger.LogFragment" + android:id="@+id/log_fragment" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1" /> + + </LinearLayout> + + <View + android:layout_width="1dp" + android:layout_height="match_parent" + android:background="@android:color/darker_gray" /> + + <FrameLayout + android:id="@+id/sample_content_fragment" + android:layout_weight="2" + android:layout_width="0px" + android:layout_height="match_parent" /> + +</LinearLayout> + + diff --git a/samples/browseable/AdapterTransition/res/layout/activity_main.xml b/samples/browseable/AdapterTransition/res/layout/activity_main.xml new file mode 100755 index 000000000..1ae4f981e --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/activity_main.xml @@ -0,0 +1,65 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/sample_main_layout"> + + <ViewAnimator + android:id="@+id/sample_output" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1"> + + <ScrollView + style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/horizontal_page_margin" + android:paddingRight="@dimen/horizontal_page_margin" + android:paddingTop="@dimen/vertical_page_margin" + android:paddingBottom="@dimen/vertical_page_margin" + android:text="@string/intro_message" /> + </ScrollView> + + <fragment + android:name="com.example.android.common.logger.LogFragment" + android:id="@+id/log_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </ViewAnimator> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@android:color/darker_gray" /> + + <FrameLayout + android:id="@+id/sample_content_fragment" + android:layout_weight="2" + android:layout_width="match_parent" + android:layout_height="0px" /> + +</LinearLayout> + diff --git a/samples/browseable/AdapterTransition/res/layout/fragment_adapter_transition.xml b/samples/browseable/AdapterTransition/res/layout/fragment_adapter_transition.xml new file mode 100644 index 000000000..22ec09060 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/fragment_adapter_transition.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context="com.example.android.adaptertransition.AdapterTransitionFragment"> + + <FrameLayout + android:id="@+id/content" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + + <FrameLayout + android:id="@+id/cover" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#f3f3f3" + android:visibility="invisible"/> + +</FrameLayout> diff --git a/samples/browseable/AdapterTransition/res/layout/fragment_meat_grid.xml b/samples/browseable/AdapterTransition/res/layout/fragment_meat_grid.xml new file mode 100644 index 000000000..9a4f7a179 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/fragment_meat_grid.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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. +--> +<GridView + android:id="@+id/abs_list_view" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:columnWidth="150dp" + android:horizontalSpacing="1dp" + android:numColumns="auto_fit" + android:padding="1dp" + android:scrollbars="none" + android:stretchMode="columnWidth" + android:verticalSpacing="1dp"/> diff --git a/samples/browseable/AdapterTransition/res/layout/fragment_meat_list.xml b/samples/browseable/AdapterTransition/res/layout/fragment_meat_list.xml new file mode 100644 index 000000000..4523b2661 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/fragment_meat_list.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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. +--> +<ListView + android:id="@+id/abs_list_view" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"/> diff --git a/samples/browseable/AdapterTransition/res/layout/item_meat_grid.xml b/samples/browseable/AdapterTransition/res/layout/item_meat_grid.xml new file mode 100644 index 000000000..d7fb77a76 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/item_meat_grid.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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 + android:id="@+id/meat_container" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="150dp"> + + <ImageView + android:id="@+id/meat_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scaleType="centerCrop" + tools:src="@drawable/p1"/> + + <TextView + android:id="@+id/meat_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_gravity="bottom|end" + android:layout_marginEnd="16dp" + android:layout_marginStart="16dp" + android:gravity="center_horizontal" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="10" + android:textColor="#ffffff" + android:textSize="24sp" + android:textStyle="bold" + tools:text="Hello"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/samples/browseable/AdapterTransition/res/layout/item_meat_list.xml b/samples/browseable/AdapterTransition/res/layout/item_meat_list.xml new file mode 100644 index 000000000..8d75b9094 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/layout/item_meat_list.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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 + android:id="@+id/meat_container" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"> + + <ImageView + android:id="@+id/meat_image" + android:layout_width="64dp" + android:layout_height="64dp" + android:scaleType="centerCrop" + tools:src="@drawable/p1"/> + + <TextView + android:id="@+id/meat_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_marginStart="?android:attr/listPreferredItemPaddingStart" + android:layout_toEndOf="@id/meat_image" + android:layout_centerInParent="true" + android:gravity="center_vertical" + android:textSize="24sp" + tools:text="Title"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/samples/browseable/AdapterTransition/res/menu/fragment_adapter_transition.xml b/samples/browseable/AdapterTransition/res/menu/fragment_adapter_transition.xml new file mode 100644 index 000000000..2a51b1133 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/menu/fragment_adapter_transition.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +Copyright 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. +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_toggle" + android:icon="@drawable/ic_action_grid" + android:showAsAction="always" + android:title="Toggle view"/> +</menu> diff --git a/samples/browseable/AdapterTransition/res/menu/main.xml b/samples/browseable/AdapterTransition/res/menu/main.xml new file mode 100644 index 000000000..b49c2c526 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/menu/main.xml @@ -0,0 +1,21 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_toggle_log" + android:showAsAction="always" + android:title="@string/sample_show_log" /> +</menu> diff --git a/samples/browseable/AdapterTransition/res/values-sw600dp/template-dimens.xml b/samples/browseable/AdapterTransition/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_huge</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/AdapterTransition/res/values-sw600dp/template-styles.xml b/samples/browseable/AdapterTransition/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceLarge</item> + <item name="android:lineSpacingMultiplier">1.2</item> + <item name="android:shadowDy">-6.5</item> + </style> + +</resources> diff --git a/samples/browseable/AdapterTransition/res/values-w820dp/dimens.xml b/samples/browseable/AdapterTransition/res/values-w820dp/dimens.xml new file mode 100644 index 000000000..63fc81644 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values-w820dp/dimens.xml @@ -0,0 +1,6 @@ +<resources> + <!-- Example customization of dimensions originally defined in res/values/dimens.xml + (such as screen margins) for screens with more than 820dp of available width. This + would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). --> + <dimen name="activity_horizontal_margin">64dp</dimen> +</resources> diff --git a/samples/browseable/AdapterTransition/res/values/base-strings.xml b/samples/browseable/AdapterTransition/res/values/base-strings.xml new file mode 100644 index 000000000..353c67e0a --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values/base-strings.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + + + +<resources> + <string name="app_name">AdapterTransition</string> + <string name="intro_message"> + <![CDATA[ + + + Transition cannot be directly applied to AdapterViews. In this sample, we demonstrate how to create a overlay layout and run a Transition on it. + + + ]]> + </string> +</resources> diff --git a/samples/browseable/AdapterTransition/res/values/dimens.xml b/samples/browseable/AdapterTransition/res/values/dimens.xml new file mode 100644 index 000000000..a0171a705 --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values/dimens.xml @@ -0,0 +1,6 @@ +<resources> + <!-- Default screen margins, per the Android Design guidelines. --> + <dimen name="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> + + </resources> diff --git a/samples/browseable/AdapterTransition/res/values/strings.xml b/samples/browseable/AdapterTransition/res/values/strings.xml new file mode 100755 index 000000000..7b9d9ec4f --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values/strings.xml @@ -0,0 +1,19 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <string name="sample_show_log">Show Log</string> + <string name="sample_hide_log">Hide Log</string> +</resources> diff --git a/samples/browseable/AdapterTransition/res/values/template-dimens.xml b/samples/browseable/AdapterTransition/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values/template-dimens.xml @@ -0,0 +1,32 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Define standard dimensions to comply with Holo-style grids and rhythm. --> + + <dimen name="margin_tiny">4dp</dimen> + <dimen name="margin_small">8dp</dimen> + <dimen name="margin_medium">16dp</dimen> + <dimen name="margin_large">32dp</dimen> + <dimen name="margin_huge">64dp</dimen> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/AdapterTransition/res/values/template-styles.xml b/samples/browseable/AdapterTransition/res/values/template-styles.xml new file mode 100644 index 000000000..404623e3d --- /dev/null +++ b/samples/browseable/AdapterTransition/res/values/template-styles.xml @@ -0,0 +1,42 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Activity themes --> + + <style name="Theme.Base" parent="android:Theme.Holo.Light" /> + + <style name="Theme.Sample" parent="Theme.Base" /> + + <style name="AppTheme" parent="Theme.Sample" /> + <!-- Widget styling --> + + <style name="Widget" /> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceMedium</item> + <item name="android:lineSpacingMultiplier">1.1</item> + </style> + + <style name="Widget.SampleMessageTile"> + <item name="android:background">@drawable/tile</item> + <item name="android:shadowColor">#7F000000</item> + <item name="android:shadowDy">-3.5</item> + <item name="android:shadowRadius">2</item> + </style> + +</resources> diff --git a/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/AdapterTransitionFragment.java b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/AdapterTransitionFragment.java new file mode 100644 index 000000000..525ed4035 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/AdapterTransitionFragment.java @@ -0,0 +1,244 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.adaptertransition; + +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.transition.AutoTransition; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.FrameLayout; +import android.widget.GridView; +import android.widget.ListView; + +/** + * Main screen for AdapterTransition sample. + */ +public class AdapterTransitionFragment extends Fragment implements Transition.TransitionListener { + + /** + * Since the transition framework requires all relevant views in a view hierarchy to be marked + * with IDs, we use this ID to mark the root view. + */ + private static final int ROOT_ID = 1; + + /** + * This is where we place our AdapterView (ListView / GridView). + */ + private FrameLayout mContent; + + /** + * This is where we carry out the transition. + */ + private FrameLayout mCover; + + /** + * This list shows our contents. It can be ListView or GridView, and we toggle between them + * using the transition framework. + */ + private AbsListView mAbsListView; + + /** + * This is our contents. + */ + private MeatAdapter mAdapter; + + public static AdapterTransitionFragment newInstance() { + return new AdapterTransitionFragment(); + } + + public AdapterTransitionFragment() { + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // We use a ListView at first + mAbsListView = (AbsListView) inflater.inflate(R.layout.fragment_meat_list, container, false); + mAdapter = new MeatAdapter(inflater, R.layout.item_meat_list); + return inflater.inflate(R.layout.fragment_adapter_transition, container, false); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + // Retaining references for FrameLayouts that we use later. + mContent = (FrameLayout) view.findViewById(R.id.content); + mCover = (FrameLayout) view.findViewById(R.id.cover); + // We are attaching the list to the screen here. + mAbsListView.setAdapter(mAdapter); + mContent.addView(mAbsListView); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.fragment_adapter_transition, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + // We change the look of the icon every time the user toggles between list and grid. + MenuItem item = menu.findItem(R.id.action_toggle); + if (null != item) { + if (mAbsListView instanceof ListView) { + item.setIcon(R.drawable.ic_action_grid); + } else { + item.setIcon(R.drawable.ic_action_list); + } + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_toggle: { + toggle(); + return true; + } + } + return false; + } + + @Override + public void onTransitionStart(Transition transition) { + } + + // BEGIN_INCLUDE(on_transition_end) + @Override + public void onTransitionEnd(Transition transition) { + // When the transition ends, we remove all the views from the overlay and hide it. + mCover.removeAllViews(); + mCover.setVisibility(View.INVISIBLE); + } + // END_INCLUDE(on_transition_end) + + @Override + public void onTransitionCancel(Transition transition) { + } + + @Override + public void onTransitionPause(Transition transition) { + } + + @Override + public void onTransitionResume(Transition transition) { + } + + /** + * Toggle the UI between ListView and GridView. + */ + private void toggle() { + // We use mCover as the overlay on which we carry out the transition. + mCover.setVisibility(View.VISIBLE); + // This FrameLayout holds all the visible views in the current list or grid. We use this as + // the starting Scene of the Transition later. + FrameLayout before = copyVisibleViews(); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); + mCover.addView(before, params); + // Swap the actual list. + swapAbsListView(); + // We also swap the icon for the toggle button. + ActivityCompat.invalidateOptionsMenu(getActivity()); + // It is now ready to start the transition. + mAbsListView.post(new Runnable() { + @Override + public void run() { + // BEGIN_INCLUDE(transition_with_listener) + Scene scene = new Scene(mCover, copyVisibleViews()); + Transition transition = new AutoTransition(); + transition.addListener(AdapterTransitionFragment.this); + TransitionManager.go(scene, transition); + // END_INCLUDE(transition_with_listener) + } + }); + } + + /** + * Swap ListView with GridView, or GridView with ListView. + */ + private void swapAbsListView() { + // We save the current scrolling position before removing the current list. + int first = mAbsListView.getFirstVisiblePosition(); + // If the current list is a GridView, we replace it with a ListView. If it is a ListView, + // a GridView. + LayoutInflater inflater = LayoutInflater.from(getActivity()); + if (mAbsListView instanceof GridView) { + mAbsListView = (AbsListView) inflater.inflate( + R.layout.fragment_meat_list, (ViewGroup) mAbsListView.getParent(), false); + mAdapter = new MeatAdapter(inflater, R.layout.item_meat_list); + } else { + mAbsListView = (AbsListView) inflater.inflate( + R.layout.fragment_meat_grid, (ViewGroup) mAbsListView.getParent(), false); + mAdapter = new MeatAdapter(inflater, R.layout.item_meat_grid); + } + mAbsListView.setAdapter(mAdapter); + // We restore the scrolling position here. + mAbsListView.setSelection(first); + // The new list is ready, and we replace the existing one with it. + mContent.removeAllViews(); + mContent.addView(mAbsListView); + } + + /** + * Copy all the visible views in the mAbsListView into a new FrameLayout and return it. + * + * @return a FrameLayout with all the visible views inside. + */ + private FrameLayout copyVisibleViews() { + // This is the FrameLayout we return afterwards. + FrameLayout layout = new FrameLayout(getActivity()); + // The transition framework requires to set ID for all views to be animated. + layout.setId(ROOT_ID); + // We only copy visible views. + int first = mAbsListView.getFirstVisiblePosition(); + int index = 0; + while (true) { + // This is one of the views that we copy. Note that the argument for getChildAt is a + // zero-oriented index, and it doesn't usually match with its position in the list. + View source = mAbsListView.getChildAt(index); + if (null == source) { + break; + } + // This is the copy of the original view. + View destination = mAdapter.getView(first + index, null, layout); + assert destination != null; + destination.setId(ROOT_ID + first + index); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + source.getWidth(), source.getHeight()); + params.leftMargin = (int) source.getX(); + params.topMargin = (int) source.getY(); + layout.addView(destination, params); + ++index; + } + return layout; + } + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MainActivity.java b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MainActivity.java new file mode 100644 index 000000000..a45632c70 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MainActivity.java @@ -0,0 +1,110 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + + + +package com.example.android.adaptertransition; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + * <p> + * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + AdapterTransitionFragment fragment = new AdapterTransitionFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/Meat.java b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/Meat.java new file mode 100644 index 000000000..bca1c5f00 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/Meat.java @@ -0,0 +1,46 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.adaptertransition; + +/** + * Sample data. + */ +public class Meat { + + public int resourceId; + public String title; + + public Meat(int resourceId, String title) { + this.resourceId = resourceId; + this.title = title; + } + + public static final Meat[] MEATS = { + new Meat(R.drawable.p1, "First"), + new Meat(R.drawable.p2, "Second"), + new Meat(R.drawable.p3, "Third"), + new Meat(R.drawable.p4, "Fourth"), + new Meat(R.drawable.p5, "Fifth"), + new Meat(R.drawable.p6, "Sixth"), + new Meat(R.drawable.p7, "Seventh"), + new Meat(R.drawable.p8, "Eighth"), + new Meat(R.drawable.p9, "Ninth"), + new Meat(R.drawable.p10, "Tenth"), + new Meat(R.drawable.p11, "Eleventh"), + }; + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MeatAdapter.java b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MeatAdapter.java new file mode 100644 index 000000000..c7630cedf --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.adaptertransition/MeatAdapter.java @@ -0,0 +1,88 @@ +/* + * Copyright 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.adaptertransition; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * This class provides data as Views. It is designed to support both ListView and GridView by + * changing a layout resource file to inflate. + */ +public class MeatAdapter extends BaseAdapter { + + private final LayoutInflater mLayoutInflater; + private final int mResourceId; + + /** + * Create a new instance of {@link MeatAdapter}. + * + * @param inflater The layout inflater. + * @param resourceId The resource ID for the layout to be used. The layout should contain an + * ImageView with ID of "meat_image" and a TextView with ID of "meat_title". + */ + public MeatAdapter(LayoutInflater inflater, int resourceId) { + mLayoutInflater = inflater; + mResourceId = resourceId; + } + + @Override + public int getCount() { + return Meat.MEATS.length; + } + + @Override + public Meat getItem(int position) { + return Meat.MEATS[position]; + } + + @Override + public long getItemId(int position) { + return Meat.MEATS[position].resourceId; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View view; + final ViewHolder holder; + if (null == convertView) { + view = mLayoutInflater.inflate(mResourceId, parent, false); + holder = new ViewHolder(); + assert view != null; + holder.image = (ImageView) view.findViewById(R.id.meat_image); + holder.title = (TextView) view.findViewById(R.id.meat_title); + view.setTag(holder); + } else { + view = convertView; + holder = (ViewHolder) view.getTag(); + } + Meat meat = getItem(position); + holder.image.setImageResource(meat.resourceId); + holder.title.setText(meat.title); + return view; + } + + private static class ViewHolder { + public ImageView image; + public TextView title; + } + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/AdapterTransition/src/com.example.android.common/activities/SampleActivityBase.java new file mode 100644 index 000000000..3228927b7 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/Log.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + * <p>When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.</p> + */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +}
\ No newline at end of file diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogNode.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * 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.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogView.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabLayout.java b/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabLayout.java new file mode 100644 index 000000000..20049e335 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabLayout.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.view; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + * <p> + * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. + * <p> + * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + * <p> + * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView { + + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + mTabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(true); + } + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + + mTabStrip.addView(tabView); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabStrip.java b/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabStrip.java new file mode 100644 index 000000000..d5bbbae59 --- /dev/null +++ b/samples/browseable/AdapterTransition/src/com.example.android.common/view/SlidingTabStrip.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.view; + +import android.R; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private final Paint mDividerPaint; + private final float mDividerHeight; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, + DEFAULT_DIVIDER_COLOR_ALPHA)); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + mDividerHeight = DEFAULT_DIVIDER_HEIGHT; + mDividerPaint = new Paint(); + mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + + // Vertical separators between the titles + int separatorTop = (height - dividerHeightPx) / 2; + for (int i = 0; i < childCount - 1; i++) { + View child = getChildAt(i); + mDividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(child.getRight(), separatorTop, child.getRight(), + separatorTop + dividerHeightPx, mDividerPaint); + } + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + private int[] mDividerColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return mDividerColors[position % mDividerColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + + void setDividerColors(int... colors) { + mDividerColors = colors; + } + } +}
\ No newline at end of file diff --git a/samples/browseable/AdvancedImmersiveMode/res/drawable-hdpi/ic_launcher.png b/samples/browseable/AdvancedImmersiveMode/res/drawable-hdpi/ic_launcher.png Binary files differindex b1efaf4b2..b96e6a546 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/drawable-hdpi/ic_launcher.png +++ b/samples/browseable/AdvancedImmersiveMode/res/drawable-hdpi/ic_launcher.png diff --git a/samples/browseable/AdvancedImmersiveMode/res/drawable-mdpi/ic_launcher.png b/samples/browseable/AdvancedImmersiveMode/res/drawable-mdpi/ic_launcher.png Binary files differindex f5f9244f2..1294d5b7b 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/drawable-mdpi/ic_launcher.png +++ b/samples/browseable/AdvancedImmersiveMode/res/drawable-mdpi/ic_launcher.png diff --git a/samples/browseable/AdvancedImmersiveMode/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/AdvancedImmersiveMode/res/drawable-xhdpi/ic_launcher.png Binary files differindex 5d07b3f06..9f101ffaf 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/drawable-xhdpi/ic_launcher.png +++ b/samples/browseable/AdvancedImmersiveMode/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/browseable/AdvancedImmersiveMode/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/AdvancedImmersiveMode/res/drawable-xxhdpi/ic_launcher.png Binary files differindex 6ef21e1f4..7a195a128 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/drawable-xxhdpi/ic_launcher.png +++ b/samples/browseable/AdvancedImmersiveMode/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/browseable/AdvancedImmersiveMode/res/layout/activity_main.xml b/samples/browseable/AdvancedImmersiveMode/res/layout/activity_main.xml index bc5a57591..1ae4f981e 100755 --- a/samples/browseable/AdvancedImmersiveMode/res/layout/activity_main.xml +++ b/samples/browseable/AdvancedImmersiveMode/res/layout/activity_main.xml @@ -14,25 +14,52 @@ limitations under the License. --> <LinearLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:id="@+id/sample_main_layout"> - <TextView android:id="@+id/sample_output" - style="@style/Widget.SampleMessage" - android:layout_weight="1" + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/sample_main_layout"> + + <ViewAnimator + android:id="@+id/sample_output" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1"> + + <ScrollView + style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/horizontal_page_margin" + android:paddingRight="@dimen/horizontal_page_margin" + android:paddingTop="@dimen/vertical_page_margin" + android:paddingBottom="@dimen/vertical_page_margin" + android:text="@string/intro_message" /> + </ScrollView> + + <fragment + android:name="com.example.android.common.logger.LogFragment" + android:id="@+id/log_fragment" android:layout_width="match_parent" - android:layout_height="match_parent" - android:text="@string/intro_message" /> + android:layout_height="match_parent" /> + + </ViewAnimator> + <View - android:layout_width="fill_parent" - android:layout_height="1dp" - android:background="@android:color/darker_gray"/> - <fragment - android:name="com.example.android.common.logger.LogFragment" - android:id="@+id/log_fragment" - android:layout_weight="1" - android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@android:color/darker_gray" /> + + <FrameLayout + android:id="@+id/sample_content_fragment" + android:layout_weight="2" + android:layout_width="match_parent" + android:layout_height="0px" /> + </LinearLayout> + diff --git a/samples/browseable/AdvancedImmersiveMode/res/menu/main.xml b/samples/browseable/AdvancedImmersiveMode/res/menu/main.xml index 2c3515dd4..b49c2c526 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/menu/main.xml +++ b/samples/browseable/AdvancedImmersiveMode/res/menu/main.xml @@ -15,7 +15,7 @@ --> <menu xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:id="@+id/sample_action" - android:showAsAction="ifRoom|withText" - android:title="@string/sample_action" /> + <item android:id="@+id/menu_toggle_log" + android:showAsAction="always" + android:title="@string/sample_show_log" /> </menu> diff --git a/samples/browseable/AdvancedImmersiveMode/res/values/base-strings.xml b/samples/browseable/AdvancedImmersiveMode/res/values/base-strings.xml index 305e12abf..8e4c710cf 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/values/base-strings.xml +++ b/samples/browseable/AdvancedImmersiveMode/res/values/base-strings.xml @@ -23,13 +23,10 @@ <![CDATA[ - \"Immersive Mode\" is a new UI mode which improves \"hide full screen\" and + \n\n\n\"Immersive Mode\", added in Android 4.4, improves the \"hide full screen\" and \"hide nav bar\" modes, by letting users swipe the bars in and out. This sample - lets the user experiment with immersive mode by enabling it and seeing how it interacts + lets the user experiment with immersive mode by seeing how it interacts with some of the other UI flags related to full-screen apps. - \n\nThis sample also lets the user choose between normal immersive mode and "sticky" - immersive mode, which removes the status bar and nav bar - a few seconds after the user has swiped them back in. ]]> diff --git a/samples/browseable/AdvancedImmersiveMode/res/values/strings.xml b/samples/browseable/AdvancedImmersiveMode/res/values/strings.xml index a65b8916a..7b9d9ec4f 100755 --- a/samples/browseable/AdvancedImmersiveMode/res/values/strings.xml +++ b/samples/browseable/AdvancedImmersiveMode/res/values/strings.xml @@ -1,22 +1,19 @@ -<?xml version="1.0" encoding="UTF-8"?> <!-- - Copyright 2013 The Android Open Source Project + Copyright 2013 The Android Open Source Project - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + 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 + 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. + 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="sample_action">Try these settings!</string> + <string name="sample_show_log">Show Log</string> + <string name="sample_hide_log">Hide Log</string> </resources> diff --git a/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml b/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml index d3f82ff64..404623e3d 100644 --- a/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml +++ b/samples/browseable/AdvancedImmersiveMode/res/values/template-styles.xml @@ -20,7 +20,9 @@ <style name="Theme.Base" parent="android:Theme.Holo.Light" /> - <style name="AppTheme" parent="Theme.Base" /> + <style name="Theme.Sample" parent="Theme.Base" /> + + <style name="AppTheme" parent="Theme.Sample" /> <!-- Widget styling --> <style name="Widget" /> @@ -37,15 +39,4 @@ <item name="android:shadowRadius">2</item> </style> - - <style name="Widget.SampleOutput"> - <item name="android:padding">@dimen/margin_medium</item> - <item name="android:textAppearance">?android:textAppearanceMedium</item> - <item name="android:lineSpacingMultiplier">1.1</item> - </style> - - <style name="Log" parent="Widget.SampleOutput"> - <item name="android:typeface">monospace</item> - </style> - </resources> diff --git a/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/AdvancedImmersiveModeFragment.java b/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/AdvancedImmersiveModeFragment.java index fe11ecb4d..d8fb0d4eb 100644 --- a/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/AdvancedImmersiveModeFragment.java +++ b/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/AdvancedImmersiveModeFragment.java @@ -17,9 +17,10 @@ package com.example.android.advancedimmersivemode; import android.os.Bundle; import android.support.v4.app.Fragment; -import android.view.MenuItem; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.CheckBox; import com.example.android.common.logger.Log; @@ -46,49 +47,136 @@ public class AdvancedImmersiveModeFragment extends Fragment { } @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { + final View flagsView = inflater.inflate(R.layout.fragment_flags, container, false); + mLowProfileCheckBox = (CheckBox) flagsView.findViewById(R.id.flag_enable_lowprof); + mHideNavCheckbox = (CheckBox) flagsView.findViewById(R.id.flag_hide_navbar); + mHideStatusBarCheckBox = (CheckBox) flagsView.findViewById(R.id.flag_hide_statbar); + mImmersiveModeCheckBox = (CheckBox) flagsView.findViewById(R.id.flag_enable_immersive); + mImmersiveModeStickyCheckBox = + (CheckBox) flagsView.findViewById(R.id.flag_enable_immersive_sticky); - final View decorView = getActivity().getWindow().getDecorView(); - ViewGroup parentView = (ViewGroup) getActivity().getWindow().getDecorView() - .findViewById(R.id.sample_main_layout); + Button toggleFlagsButton = (Button) flagsView.findViewById(R.id.btn_changeFlags); + toggleFlagsButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + toggleUiFlags(); + } + }); - mLowProfileCheckBox = new CheckBox(getActivity()); - mLowProfileCheckBox.setText("Enable Low Profile mode."); - parentView.addView(mLowProfileCheckBox); + Button presetsImmersiveModeButton = (Button) flagsView.findViewById(R.id.btn_immersive); + presetsImmersiveModeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { - mHideNavCheckbox = new CheckBox(getActivity()); - mHideNavCheckbox.setChecked(true); - mHideNavCheckbox.setText("Hide Navigation bar"); - parentView.addView(mHideNavCheckbox); + // BEGIN_INCLUDE(immersive_presets) + // For immersive mode, the FULLSCREEN, HIDE_HAVIGATION and IMMERSIVE + // flags should be set (you can use IMMERSIVE_STICKY instead of IMMERSIVE + // as appropriate for your app). The LOW_PROFILE flag should be cleared. - mHideStatusBarCheckBox = new CheckBox(getActivity()); - mHideStatusBarCheckBox.setChecked(true); - mHideStatusBarCheckBox.setText("Hide Status Bar"); - parentView.addView(mHideStatusBarCheckBox); + // Immersive mode is primarily for situations where the user will be + // interacting with the screen, like games or reading books. + int uiOptions = flagsView.getSystemUiVisibility(); + uiOptions &= ~View.SYSTEM_UI_FLAG_LOW_PROFILE; + uiOptions |= View.SYSTEM_UI_FLAG_FULLSCREEN; + uiOptions |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + uiOptions |= View.SYSTEM_UI_FLAG_IMMERSIVE; + uiOptions &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + flagsView.setSystemUiVisibility(uiOptions); + // END_INCLUDE(immersive_presets) - mImmersiveModeCheckBox = new CheckBox(getActivity()); - mImmersiveModeCheckBox.setText("Enable Immersive Mode."); - parentView.addView(mImmersiveModeCheckBox); + dumpFlagStateToLog(uiOptions); - mImmersiveModeStickyCheckBox = new CheckBox(getActivity()); - mImmersiveModeStickyCheckBox.setText("Enable Immersive Mode (Sticky)"); - parentView.addView(mImmersiveModeStickyCheckBox); + // The below code just updates the checkboxes to reflect which flags have been set. + mLowProfileCheckBox.setChecked(false); + mHideNavCheckbox.setChecked(true); + mHideStatusBarCheckBox.setChecked(true); + mImmersiveModeCheckBox.setChecked(true); + mImmersiveModeStickyCheckBox.setChecked(false); + } + }); + + Button presetsLeanbackModeButton = (Button) flagsView.findViewById(R.id.btn_leanback); + presetsLeanbackModeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // BEGIN_INCLUDE(leanback_presets) + // For leanback mode, only the HIDE_NAVE and HIDE_STATUSBAR flags + // should be checked. In this case IMMERSIVE should *not* be set, + // since this mode is left as soon as the user touches the screen. + int uiOptions = flagsView.getSystemUiVisibility(); + uiOptions &= ~View.SYSTEM_UI_FLAG_LOW_PROFILE; + uiOptions |= View.SYSTEM_UI_FLAG_FULLSCREEN; + uiOptions |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; + uiOptions &= ~View.SYSTEM_UI_FLAG_IMMERSIVE; + uiOptions &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + flagsView.setSystemUiVisibility(uiOptions); + // END_INCLUDE(leanback_presets) + + dumpFlagStateToLog(uiOptions); + + // The below code just updates the checkboxes to reflect which flags have been set. + mLowProfileCheckBox.setChecked(false); + mHideNavCheckbox.setChecked(true); + mHideStatusBarCheckBox.setChecked(true); + mImmersiveModeCheckBox.setChecked(false); + mImmersiveModeStickyCheckBox.setChecked(false); + } + }); + + // Setting these flags makes the content appear under the navigation + // bars, so that showing/hiding the nav bars doesn't resize the content + // window, which can be jarring. + int uiOptions = flagsView.getSystemUiVisibility(); + uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + uiOptions |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + flagsView.setSystemUiVisibility(uiOptions); + + return flagsView; } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.sample_action) { - toggleImmersiveMode(); + /** + * Helper method to dump flag state to the log. + * @param uiFlags Set of UI flags to inspect + */ + public void dumpFlagStateToLog(int uiFlags) { + if ((uiFlags & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { + Log.i(TAG, "SYSTEM_UI_FLAG_LOW_PROFILE is set"); + } else { + Log.i(TAG, "SYSTEM_UI_FLAG_LOW_PROFILE is unset"); + } + + if ((uiFlags & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0) { + Log.i(TAG, "SYSTEM_UI_FLAG_FULLSCREEN is set"); + } else { + Log.i(TAG, "SYSTEM_UI_FLAG_FULLSCREEN is unset"); + } + + if ((uiFlags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0) { + Log.i(TAG, "SYSTEM_UI_FLAG_HIDE_NAVIGATION is set"); + } else { + Log.i(TAG, "SYSTEM_UI_FLAG_HIDE_NAVIGATION is unset"); + } + + if ((uiFlags & View.SYSTEM_UI_FLAG_IMMERSIVE) != 0) { + Log.i(TAG, "SYSTEM_UI_FLAG_IMMERSIVE is set"); + } else { + Log.i(TAG, "SYSTEM_UI_FLAG_IMMERSIVE is unset"); + } + + if ((uiFlags & View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0) { + Log.i(TAG, "SYSTEM_UI_FLAG_IMMERSIVE_STICKY is set"); + } else { + Log.i(TAG, "SYSTEM_UI_FLAG_IMMERSIVE_STICKY is unset"); } - return true; } /** * Detects and toggles immersive mode (also known as "hidey bar" mode). */ - public void toggleImmersiveMode() { + public void toggleUiFlags() { // BEGIN_INCLUDE (get_current_ui_flags) // The "Decor View" is the parent view of the Activity. It's also conveniently the easiest @@ -168,7 +256,8 @@ public class AdvancedImmersiveModeFragment extends Fragment { // BEGIN_INCLUDE (set_ui_flags) //Set the new UI flags. decorView.setSystemUiVisibility(newUiOptions); - Log.i(TAG, "Current height: " + decorView.getHeight() + ", width: " + decorView.getWidth()); // END_INCLUDE (set_ui_flags) + + dumpFlagStateToLog(uiOptions); } } diff --git a/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/MainActivity.java b/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/MainActivity.java index 0ebe8784f..e32355760 100644 --- a/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/MainActivity.java +++ b/samples/browseable/AdvancedImmersiveMode/src/com.example.android.advancedimmersivemode/MainActivity.java @@ -22,6 +22,8 @@ package com.example.android.advancedimmersivemode; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; import com.example.android.common.activities.SampleActivityBase; import com.example.android.common.logger.Log; @@ -30,26 +32,28 @@ import com.example.android.common.logger.LogWrapper; import com.example.android.common.logger.MessageOnlyLogFilter; /** - * A simple launcher activity containing a summary sample description - * and a few action bar buttons. + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + * <p> + * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. */ public class MainActivity extends SampleActivityBase { public static final String TAG = "MainActivity"; - public static final String FRAGTAG = "AdvancedImmersiveModeFragment"; + // Whether the Log Fragment is currently shown + private boolean mLogShown; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - if (getSupportFragmentManager().findFragmentByTag(FRAGTAG) == null ) { - FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); - AdvancedImmersiveModeFragment fragment = new AdvancedImmersiveModeFragment(); - transaction.add(fragment, FRAGTAG); - transaction.commit(); - } + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + AdvancedImmersiveModeFragment fragment = new AdvancedImmersiveModeFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); } @Override @@ -58,6 +62,32 @@ public class MainActivity extends SampleActivityBase { return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + /** Create a chain of targets that will receive log data */ @Override public void initializeLogging() { diff --git a/samples/browseable/BasicTransition/AndroidManifest.xml b/samples/browseable/BasicTransition/AndroidManifest.xml new file mode 100644 index 000000000..b4698d688 --- /dev/null +++ b/samples/browseable/BasicTransition/AndroidManifest.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.basictransition" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="19" /> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name" + android:theme="@style/AppTheme" > + <activity + android:name=".MainActivity" + android:label="@string/app_name" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest> diff --git a/samples/browseable/BasicTransition/_index.jd b/samples/browseable/BasicTransition/_index.jd new file mode 100644 index 000000000..e65ec530f --- /dev/null +++ b/samples/browseable/BasicTransition/_index.jd @@ -0,0 +1,13 @@ + + + +page.tags="BasicTransition" +sample.group=UI +@jd:body + +<p> + + This sample demonstrates the basic use of the transition framework introduced in KitKat. + Select each of the RadioButtons to switch between the Scenes. + + </p> diff --git a/samples/browseable/BasicTransition/res/drawable-hdpi/ic_launcher.png b/samples/browseable/BasicTransition/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..0f5d36076 --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable-hdpi/ic_launcher.png diff --git a/samples/browseable/BasicTransition/res/drawable-hdpi/tile.9.png b/samples/browseable/BasicTransition/res/drawable-hdpi/tile.9.png Binary files differnew file mode 100644 index 000000000..135862883 --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/BasicTransition/res/drawable-mdpi/ic_launcher.png b/samples/browseable/BasicTransition/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..72d85c944 --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable-mdpi/ic_launcher.png diff --git a/samples/browseable/BasicTransition/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/BasicTransition/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..cf93e6994 --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/browseable/BasicTransition/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/BasicTransition/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..149a98491 --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/browseable/BasicTransition/res/drawable/oval.xml b/samples/browseable/BasicTransition/res/drawable/oval.xml new file mode 100644 index 000000000..07f3abd9e --- /dev/null +++ b/samples/browseable/BasicTransition/res/drawable/oval.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + <solid android:color="#0000ff"/> +</shape> diff --git a/samples/browseable/BasicTransition/res/layout-w720dp/activity_main.xml b/samples/browseable/BasicTransition/res/layout-w720dp/activity_main.xml new file mode 100755 index 000000000..c9a52f621 --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout-w720dp/activity_main.xml @@ -0,0 +1,73 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/sample_main_layout"> + + <LinearLayout + android:id="@+id/sample_output" + android:layout_width="0px" + android:layout_height="match_parent" + android:layout_weight="1" + android:orientation="vertical"> + + <FrameLayout + style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/margin_medium" + android:paddingRight="@dimen/margin_medium" + android:paddingTop="@dimen/margin_large" + android:paddingBottom="@dimen/margin_large" + android:text="@string/intro_message" /> + </FrameLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@android:color/darker_gray" /> + + <fragment + android:name="com.example.android.common.logger.LogFragment" + android:id="@+id/log_fragment" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1" /> + + </LinearLayout> + + <View + android:layout_width="1dp" + android:layout_height="match_parent" + android:background="@android:color/darker_gray" /> + + <FrameLayout + android:id="@+id/sample_content_fragment" + android:layout_weight="2" + android:layout_width="0px" + android:layout_height="match_parent" /> + +</LinearLayout> + + diff --git a/samples/browseable/BasicTransition/res/layout/activity_basic_transition.xml b/samples/browseable/BasicTransition/res/layout/activity_basic_transition.xml new file mode 100644 index 000000000..f9a4cd287 --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/activity_basic_transition.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.example.android.basictransition.BasicTransitionActivity"/> diff --git a/samples/browseable/BasicTransition/res/layout/activity_main.xml b/samples/browseable/BasicTransition/res/layout/activity_main.xml new file mode 100755 index 000000000..1ae4f981e --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/activity_main.xml @@ -0,0 +1,65 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/sample_main_layout"> + + <ViewAnimator + android:id="@+id/sample_output" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1"> + + <ScrollView + style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/horizontal_page_margin" + android:paddingRight="@dimen/horizontal_page_margin" + android:paddingTop="@dimen/vertical_page_margin" + android:paddingBottom="@dimen/vertical_page_margin" + android:text="@string/intro_message" /> + </ScrollView> + + <fragment + android:name="com.example.android.common.logger.LogFragment" + android:id="@+id/log_fragment" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </ViewAnimator> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@android:color/darker_gray" /> + + <FrameLayout + android:id="@+id/sample_content_fragment" + android:layout_weight="2" + android:layout_width="match_parent" + android:layout_height="0px" /> + +</LinearLayout> + diff --git a/samples/browseable/BasicTransition/res/layout/fragment_basic_transition.xml b/samples/browseable/BasicTransition/res/layout/fragment_basic_transition.xml new file mode 100644 index 000000000..98999c8eb --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/fragment_basic_transition.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin" + tools:context="com.example.android.basictransition.BasicTransitionFragment"> + + <RadioGroup + android:id="@+id/select_scene" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:orientation="horizontal"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/scene"/> + + <RadioButton + android:id="@+id/select_scene_1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:checked="true" + android:text="@string/scene_1"/> + + <RadioButton + android:id="@+id/select_scene_2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/scene_2"/> + + <RadioButton + android:id="@+id/select_scene_3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/scene_3"/> + + <RadioButton + android:id="@+id/select_scene_4" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/scene_4"/> + + </RadioGroup> + + <FrameLayout + android:id="@+id/scene_root" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <include layout="@layout/scene1"/> + + </FrameLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/samples/browseable/BasicTransition/res/layout/scene1.xml b/samples/browseable/BasicTransition/res/layout/scene1.xml new file mode 100644 index 000000000..005bf3be2 --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/scene1.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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 + android:id="@+id/container" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <View + android:id="@+id/transition_square" + android:layout_width="@dimen/square_size_normal" + android:layout_height="@dimen/square_size_normal" + android:background="#990000" + android:gravity="center"/> + + <ImageView + android:id="@+id/transition_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/transition_square" + android:src="@drawable/ic_launcher"/> + + <ImageView + android:id="@+id/transition_oval" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_below="@id/transition_image" + android:src="@drawable/oval"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/samples/browseable/BasicTransition/res/layout/scene2.xml b/samples/browseable/BasicTransition/res/layout/scene2.xml new file mode 100644 index 000000000..38a809a54 --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/scene2.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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 + android:id="@+id/container" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <View + android:id="@+id/transition_square" + android:layout_width="@dimen/square_size_normal" + android:layout_height="@dimen/square_size_normal" + android:layout_alignParentBottom="true" + android:background="#990000" + android:gravity="center"/> + + <ImageView + android:id="@+id/transition_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:src="@drawable/ic_launcher"/> + + <ImageView + android:id="@+id/transition_oval" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_centerHorizontal="true" + android:src="@drawable/oval"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/samples/browseable/BasicTransition/res/layout/scene3.xml b/samples/browseable/BasicTransition/res/layout/scene3.xml new file mode 100644 index 000000000..06246dab2 --- /dev/null +++ b/samples/browseable/BasicTransition/res/layout/scene3.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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 + android:id="@+id/container" + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <View + android:id="@+id/transition_square" + android:layout_width="@dimen/square_size_normal" + android:layout_height="@dimen/square_size_normal" + android:layout_centerHorizontal="true" + android:background="#990000" + android:gravity="center"/> + + <ImageView + android:id="@+id/transition_image" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:src="@drawable/ic_launcher"/> + + <ImageView + android:id="@+id/transition_oval" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:src="@drawable/oval"/> + + <TextView + android:id="@+id/transition_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:text="@string/this_is_scene_3" + android:textAppearance="?android:attr/textAppearanceLarge"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/samples/browseable/BasicTransition/res/menu/main.xml b/samples/browseable/BasicTransition/res/menu/main.xml new file mode 100644 index 000000000..b49c2c526 --- /dev/null +++ b/samples/browseable/BasicTransition/res/menu/main.xml @@ -0,0 +1,21 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/menu_toggle_log" + android:showAsAction="always" + android:title="@string/sample_show_log" /> +</menu> diff --git a/samples/browseable/BasicTransition/res/transition/changebounds_fadein_together.xml b/samples/browseable/BasicTransition/res/transition/changebounds_fadein_together.xml new file mode 100644 index 000000000..062e01235 --- /dev/null +++ b/samples/browseable/BasicTransition/res/transition/changebounds_fadein_together.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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. +--> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"> + <changeBounds/> + <fade android:fadingMode="fade_in"> + <targets> + <target android:targetId="@id/transition_title" /> + </targets> + </fade> +</transitionSet> diff --git a/samples/browseable/BasicTransition/res/transition/scene3_transition_manager.xml b/samples/browseable/BasicTransition/res/transition/scene3_transition_manager.xml new file mode 100644 index 000000000..6189d6130 --- /dev/null +++ b/samples/browseable/BasicTransition/res/transition/scene3_transition_manager.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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. +--> +<transitionManager xmlns:android="http://schemas.android.com/apk/res/android"> + <transition + android:toScene="@layout/scene3" + android:transition="@transition/changebounds_fadein_together"/> +</transitionManager> diff --git a/samples/browseable/BasicTransition/res/values-sw600dp/template-dimens.xml b/samples/browseable/BasicTransition/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/BasicTransition/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_huge</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/BasicTransition/res/values-sw600dp/template-styles.xml b/samples/browseable/BasicTransition/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/BasicTransition/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceLarge</item> + <item name="android:lineSpacingMultiplier">1.2</item> + <item name="android:shadowDy">-6.5</item> + </style> + +</resources> diff --git a/samples/browseable/BasicTransition/res/values-w820dp/dimens.xml b/samples/browseable/BasicTransition/res/values-w820dp/dimens.xml new file mode 100644 index 000000000..21e296886 --- /dev/null +++ b/samples/browseable/BasicTransition/res/values-w820dp/dimens.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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> + <!-- Example customization of dimensions originally defined in res/values/dimens.xml + (such as screen margins) for screens with more than 820dp of available width. This + would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). --> + <dimen name="activity_horizontal_margin">64dp</dimen> +</resources> diff --git a/samples/browseable/BasicTransition/res/values/base-strings.xml b/samples/browseable/BasicTransition/res/values/base-strings.xml new file mode 100644 index 000000000..466e59069 --- /dev/null +++ b/samples/browseable/BasicTransition/res/values/base-strings.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + + + +<resources> + <string name="app_name">BasicTransition</string> + <string name="intro_message"> + <![CDATA[ + + + This sample demonstrates the basic use of the transition framework introduced in KitKat. + Select each of the RadioButtons to switch between the Scenes. + + + ]]> + </string> +</resources> diff --git a/samples/browseable/BasicTransition/res/values/dimens.xml b/samples/browseable/BasicTransition/res/values/dimens.xml new file mode 100644 index 000000000..45ccdbc37 --- /dev/null +++ b/samples/browseable/BasicTransition/res/values/dimens.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 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. + 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="activity_horizontal_margin">16dp</dimen> + <dimen name="activity_vertical_margin">16dp</dimen> + <dimen name="square_size_normal">50dp</dimen> + <dimen name="square_size_expanded">100dp</dimen> +</resources> diff --git a/samples/browseable/BasicTransition/res/values/strings.xml b/samples/browseable/BasicTransition/res/values/strings.xml new file mode 100755 index 000000000..7b9d9ec4f --- /dev/null +++ b/samples/browseable/BasicTransition/res/values/strings.xml @@ -0,0 +1,19 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<resources> + <string name="sample_show_log">Show Log</string> + <string name="sample_hide_log">Hide Log</string> +</resources> diff --git a/samples/browseable/BasicTransition/res/values/template-dimens.xml b/samples/browseable/BasicTransition/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/BasicTransition/res/values/template-dimens.xml @@ -0,0 +1,32 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Define standard dimensions to comply with Holo-style grids and rhythm. --> + + <dimen name="margin_tiny">4dp</dimen> + <dimen name="margin_small">8dp</dimen> + <dimen name="margin_medium">16dp</dimen> + <dimen name="margin_large">32dp</dimen> + <dimen name="margin_huge">64dp</dimen> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/BasicTransition/res/values/template-styles.xml b/samples/browseable/BasicTransition/res/values/template-styles.xml new file mode 100644 index 000000000..404623e3d --- /dev/null +++ b/samples/browseable/BasicTransition/res/values/template-styles.xml @@ -0,0 +1,42 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Activity themes --> + + <style name="Theme.Base" parent="android:Theme.Holo.Light" /> + + <style name="Theme.Sample" parent="Theme.Base" /> + + <style name="AppTheme" parent="Theme.Sample" /> + <!-- Widget styling --> + + <style name="Widget" /> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceMedium</item> + <item name="android:lineSpacingMultiplier">1.1</item> + </style> + + <style name="Widget.SampleMessageTile"> + <item name="android:background">@drawable/tile</item> + <item name="android:shadowColor">#7F000000</item> + <item name="android:shadowDy">-3.5</item> + <item name="android:shadowRadius">2</item> + </style> + +</resources> diff --git a/samples/browseable/BasicTransition/src/com.example.android.basictransition/BasicTransitionFragment.java b/samples/browseable/BasicTransition/src/com.example.android.basictransition/BasicTransitionFragment.java new file mode 100644 index 000000000..e67603dce --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.basictransition/BasicTransitionFragment.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.basictransition; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.transition.Scene; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RadioGroup; + +public class BasicTransitionFragment extends Fragment + implements RadioGroup.OnCheckedChangeListener { + + // We transition between these Scenes + private Scene mScene1; + private Scene mScene2; + private Scene mScene3; + + /** A custom TransitionManager */ + private TransitionManager mTransitionManagerForScene3; + + /** Transitions take place in this ViewGroup. We retain this for the dynamic transition on scene 4. */ + private ViewGroup mSceneRoot; + + public static BasicTransitionFragment newInstance() { + return new BasicTransitionFragment(); + } + + public BasicTransitionFragment() { + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_basic_transition, container, false); + assert view != null; + RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.select_scene); + radioGroup.setOnCheckedChangeListener(this); + mSceneRoot = (ViewGroup) view.findViewById(R.id.scene_root); + + // BEGIN_INCLUDE(instantiation_from_view) + // A Scene can be instantiated from a live view hierarchy. + mScene1 = new Scene(mSceneRoot, (ViewGroup) mSceneRoot.findViewById(R.id.container)); + // END_INCLUDE(instantiation_from_view) + + // BEGIN_INCLUDE(instantiation_from_resource) + // You can also inflate a generate a Scene from a layout resource file. + mScene2 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene2, getActivity()); + // END_INCLUDE(instantiation_from_resource) + + // Another scene from a layout resource file. + mScene3 = Scene.getSceneForLayout(mSceneRoot, R.layout.scene3, getActivity()); + + // BEGIN_INCLUDE(custom_transition_manager) + // We create a custom TransitionManager for Scene 3, in which ChangeBounds and Fade + // take place at the same time. + mTransitionManagerForScene3 = TransitionInflater.from(getActivity()) + .inflateTransitionManager(R.transition.scene3_transition_manager, mSceneRoot); + // END_INCLUDE(custom_transition_manager) + + return view; + } + + @Override + public void onCheckedChanged(RadioGroup group, int checkedId) { + switch (checkedId) { + case R.id.select_scene_1: { + // BEGIN_INCLUDE(transition_simple) + // You can start an automatic transition with TransitionManager.go(). + TransitionManager.go(mScene1); + // END_INCLUDE(transition_simple) + break; + } + case R.id.select_scene_2: { + TransitionManager.go(mScene2); + break; + } + case R.id.select_scene_3: { + // BEGIN_INCLUDE(transition_custom) + // You can also start a transition with a custom TransitionManager. + mTransitionManagerForScene3.transitionTo(mScene3); + // END_INCLUDE(transition_custom) + break; + } + case R.id.select_scene_4: { + // BEGIN_INCLUDE(transition_dynamic) + // Alternatively, transition can be invoked dynamically without a Scene. + // For this, we first call TransitionManager.beginDelayedTransition(). + TransitionManager.beginDelayedTransition(mSceneRoot); + // Then, we can just change view properties as usual. + View square = mSceneRoot.findViewById(R.id.transition_square); + ViewGroup.LayoutParams params = square.getLayoutParams(); + int newSize = getResources().getDimensionPixelSize(R.dimen.square_size_expanded); + params.width = newSize; + params.height = newSize; + square.setLayoutParams(params); + // END_INCLUDE(transition_dynamic) + break; + } + } + } + +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.basictransition/MainActivity.java b/samples/browseable/BasicTransition/src/com.example.android.basictransition/MainActivity.java new file mode 100644 index 000000000..1e7c3017b --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.basictransition/MainActivity.java @@ -0,0 +1,110 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + + + +package com.example.android.basictransition; + +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ViewAnimator; + +import com.example.android.common.activities.SampleActivityBase; +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogFragment; +import com.example.android.common.logger.LogWrapper; +import com.example.android.common.logger.MessageOnlyLogFilter; + +/** + * A simple launcher activity containing a summary sample description, sample log and a custom + * {@link android.support.v4.app.Fragment} which can display a view. + * <p> + * For devices with displays with a width of 720dp or greater, the sample log is always visible, + * on other devices it's visibility is controlled by an item on the Action Bar. + */ +public class MainActivity extends SampleActivityBase { + + public static final String TAG = "MainActivity"; + + // Whether the Log Fragment is currently shown + private boolean mLogShown; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); + BasicTransitionFragment fragment = new BasicTransitionFragment(); + transaction.replace(R.id.sample_content_fragment, fragment); + transaction.commit(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + MenuItem logToggle = menu.findItem(R.id.menu_toggle_log); + logToggle.setVisible(findViewById(R.id.sample_output) instanceof ViewAnimator); + logToggle.setTitle(mLogShown ? R.string.sample_hide_log : R.string.sample_show_log); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch(item.getItemId()) { + case R.id.menu_toggle_log: + mLogShown = !mLogShown; + ViewAnimator output = (ViewAnimator) findViewById(R.id.sample_output); + if (mLogShown) { + output.setDisplayedChild(1); + } else { + output.setDisplayedChild(0); + } + supportInvalidateOptionsMenu(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** Create a chain of targets that will receive log data */ + @Override + public void initializeLogging() { + // Wraps Android's native log framework. + LogWrapper logWrapper = new LogWrapper(); + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + Log.setLogNode(logWrapper); + + // Filter strips out everything except the message text. + MessageOnlyLogFilter msgFilter = new MessageOnlyLogFilter(); + logWrapper.setNext(msgFilter); + + // On screen logging via a fragment with a TextView. + LogFragment logFragment = (LogFragment) getSupportFragmentManager() + .findFragmentById(R.id.log_fragment); + msgFilter.setNext(logFragment.getLogView()); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/activities/SampleActivityBase.java b/samples/browseable/BasicTransition/src/com.example.android.common/activities/SampleActivityBase.java new file mode 100644 index 000000000..3228927b7 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/activities/SampleActivityBase.java @@ -0,0 +1,52 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.common.activities; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; + +import com.example.android.common.logger.Log; +import com.example.android.common.logger.LogWrapper; + +/** + * Base launcher activity, to handle most of the common plumbing for samples. + */ +public class SampleActivityBase extends FragmentActivity { + + public static final String TAG = "SampleActivityBase"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + protected void onStart() { + super.onStart(); + initializeLogging(); + } + + /** Set up targets to receive log data */ + public void initializeLogging() { + // Using Log, front-end to the logging chain, emulates android.util.log method signatures. + // Wraps Android's native log framework + LogWrapper logWrapper = new LogWrapper(); + Log.setLogNode(logWrapper); + + Log.i(TAG, "Ready"); + } +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/Log.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + * <p>When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.</p> + */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogFragment.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +}
\ No newline at end of file diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogNode.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * 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.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogView.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogWrapper.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java b/samples/browseable/BasicTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabLayout.java b/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabLayout.java new file mode 100644 index 000000000..20049e335 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabLayout.java @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.view; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Build; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.HorizontalScrollView; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give constant feedback as to + * the user's scroll progress. + * <p> + * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is being used for. + * <p> + * The colors can be customized in two ways. The first and simplest is to provide an array of colors + * via {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)}. The + * alternative is via the {@link TabColorizer} interface which provides you complete control over + * which color is used for any individual position. + * <p> + * The views used as tabs can be customized by calling {@link #setCustomTabView(int, int)}, + * providing the layout ID of your custom layout. + */ +public class SlidingTabLayout extends HorizontalScrollView { + + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} is selected. + */ + int getIndicatorColor(int position); + + /** + * @return return the color of the divider drawn to the right of {@code position}. + */ + int getDividerColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + + private int mTitleOffset; + + private int mTabViewLayoutId; + private int mTabViewTextViewId; + + private ViewPager mViewPager; + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final SlidingTabStrip mTabStrip; + + public SlidingTabLayout(Context context) { + this(context, null); + } + + public SlidingTabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingTabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new SlidingTabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple custmisation then you can use + * {@link #setSelectedIndicatorColors(int...)} and {@link #setDividerColors(int...)} to achieve + * similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors are treated as a + * circular array. Providing one color will mean that all tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Sets the colors to be used for tab dividers. These colors are treated as a circular array. + * Providing one color will mean that all tabs are indicated with the same color. + */ + public void setDividerColors(int... colors) { + mTabStrip.setDividerColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using {@link SlidingTabLayout} you are + * required to set any {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Set the custom layout to be inflated for the tab views. + * + * @param layoutResId Layout id to be inflated + * @param textViewId id of the {@link TextView} in the inflated view + */ + public void setCustomTabView(int layoutResId, int textViewId) { + mTabViewLayoutId = layoutResId; + mTabViewTextViewId = textViewId; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the pager content + * (number of tabs and tab titles) does not change after this call has been made. + */ + public void setViewPager(ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + if (viewPager != null) { + viewPager.setOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Create a default view to be used for tabs. This is called if a custom tab view is not set via + * {@link #setCustomTabView(int, int)}. + */ + protected TextView createDefaultTabView(Context context) { + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + // If we're running on Honeycomb or newer, then we can use the Theme's + // selectableItemBackground to ensure that the View has a pressed state + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, + outValue, true); + textView.setBackgroundResource(outValue.resourceId); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // If we're running on ICS or newer, enable all-caps to match the Action Bar tab style + textView.setAllCaps(true); + } + + int padding = (int) (TAB_VIEW_PADDING_DIPS * getResources().getDisplayMetrics().density); + textView.setPadding(padding, padding, padding, padding); + + return textView; + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final View.OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + TextView tabTitleView = null; + + if (mTabViewLayoutId != 0) { + // If there is a custom tab view layout id set, try and inflate it + tabView = LayoutInflater.from(getContext()).inflate(mTabViewLayoutId, mTabStrip, + false); + tabTitleView = (TextView) tabView.findViewById(mTabViewTextViewId); + } + + if (tabView == null) { + tabView = createDefaultTabView(getContext()); + } + + if (tabTitleView == null && TextView.class.isInstance(tabView)) { + tabTitleView = (TextView) tabView; + } + + tabTitleView.setText(adapter.getPageTitle(i)); + tabView.setOnClickListener(tabClickListener); + + mTabStrip.addView(tabView); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) + ? (int) (positionOffset * selectedTitle.getWidth()) + : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, + positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements View.OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } + +} diff --git a/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabStrip.java b/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabStrip.java new file mode 100644 index 000000000..d5bbbae59 --- /dev/null +++ b/samples/browseable/BasicTransition/src/com.example.android.common/view/SlidingTabStrip.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.view; + +import android.R; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class SlidingTabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 2; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 8; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private static final int DEFAULT_DIVIDER_THICKNESS_DIPS = 1; + private static final byte DEFAULT_DIVIDER_COLOR_ALPHA = 0x20; + private static final float DEFAULT_DIVIDER_HEIGHT = 0.5f; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private final Paint mDividerPaint; + private final float mDividerHeight; + + private int mSelectedPosition; + private float mSelectionOffset; + + private SlidingTabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + SlidingTabStrip(Context context) { + this(context, null); + } + + SlidingTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + mDefaultTabColorizer.setDividerColors(setColorAlpha(themeForegroundColor, + DEFAULT_DIVIDER_COLOR_ALPHA)); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + mDividerHeight = DEFAULT_DIVIDER_HEIGHT; + mDividerPaint = new Paint(); + mDividerPaint.setStrokeWidth((int) (DEFAULT_DIVIDER_THICKNESS_DIPS * density)); + } + + void setCustomTabColorizer(SlidingTabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setDividerColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setDividerColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final int dividerHeightPx = (int) (Math.min(Math.max(0f, mDividerHeight), 1f) * height); + final SlidingTabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + + // Vertical separators between the titles + int separatorTop = (height - dividerHeightPx) / 2; + for (int i = 0; i < childCount - 1; i++) { + View child = getChildAt(i); + mDividerPaint.setColor(tabColorizer.getDividerColor(i)); + canvas.drawLine(child.getRight(), separatorTop, child.getRight(), + separatorTop + dividerHeightPx, mDividerPaint); + } + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements SlidingTabLayout.TabColorizer { + private int[] mIndicatorColors; + private int[] mDividerColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + @Override + public final int getDividerColor(int position) { + return mDividerColors[position % mDividerColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + + void setDividerColors(int... colors) { + mDividerColors = colors; + } + } +}
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/AndroidManifest.xml b/samples/browseable/DisplayingBitmaps/AndroidManifest.xml new file mode 100644 index 000000000..23db308a3 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/AndroidManifest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.displayingbitmaps" + android:versionCode="1" + android:versionName="1.0"> + + <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="19" /> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + + <application android:allowBackup="true" + android:label="@string/app_name" + android:description="@string/intro_message" + android:icon="@drawable/ic_launcher" + android:theme="@style/AppThemeDark"> + + <activity android:name=".ui.ImageGridActivity" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + <activity + android:name=".ui.ImageDetailActivity" + android:label="@string/app_name" + android:parentActivityName=".ui.ImageGridActivity" + android:theme="@style/AppThemeDark.FullScreen" > + <meta-data android:name="android.support.PARENT_ACTIVITY" + android:value=".ui.ImageGridActivity" /> + </activity> + + </application> + +</manifest> diff --git a/samples/browseable/DisplayingBitmaps/_index.jd b/samples/browseable/DisplayingBitmaps/_index.jd new file mode 100644 index 000000000..ccb4fa8f2 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/_index.jd @@ -0,0 +1,18 @@ + + + +page.tags="DisplayingBitmaps" +sample.group=UI +@jd:body + +<p> + + This is a sample application for the Android Training class + "Displaying Bitmaps Efficiently" + (http://developer.android.com/training/displaying-bitmaps/).\n\n + + It demonstrates how to load large bitmaps efficiently off the main UI thread, caching + bitmaps (both in memory and on disk), managing bitmap memory and displaying bitmaps + in UI elements such as ViewPager and ListView/GridView. + + </p> diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/ic_launcher.png b/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..75b3c9781 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/ic_launcher.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/tile.9.png b/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/tile.9.png Binary files differnew file mode 100644 index 000000000..135862883 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-mdpi/ic_launcher.png b/samples/browseable/DisplayingBitmaps/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..0c9c11af9 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-mdpi/ic_launcher.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-nodpi/empty_photo.png b/samples/browseable/DisplayingBitmaps/res/drawable-nodpi/empty_photo.png Binary files differnew file mode 100644 index 000000000..da1478a51 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-nodpi/empty_photo.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/DisplayingBitmaps/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..7c5aeed04 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/DisplayingBitmaps/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..91b0f960b --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/browseable/DisplayingBitmaps/res/drawable/photogrid_list_selector.xml b/samples/browseable/DisplayingBitmaps/res/drawable/photogrid_list_selector.xml new file mode 100644 index 000000000..d331c3991 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/drawable/photogrid_list_selector.xml @@ -0,0 +1,33 @@ +<!-- + 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + + <item android:state_pressed="true"> + <shape> + <solid android:color="@color/grid_state_pressed" /> + </shape> + </item> + + <item android:state_focused="true"> + <shape> + <solid android:color="@color/grid_state_focused" /> + </shape> + </item> + + <item android:drawable="@android:color/transparent" /> + +</selector> diff --git a/samples/browseable/DisplayingBitmaps/res/layout/activity_main.xml b/samples/browseable/DisplayingBitmaps/res/layout/activity_main.xml new file mode 100755 index 000000000..be1aa49d9 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/layout/activity_main.xml @@ -0,0 +1,36 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/horizontal_page_margin" + android:layout_marginRight="@dimen/horizontal_page_margin" + android:layout_marginTop="@dimen/vertical_page_margin" + android:layout_marginBottom="@dimen/vertical_page_margin" + android:text="@string/intro_message" /> + </LinearLayout> +</LinearLayout> diff --git a/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml new file mode 100644 index 000000000..97ac520cc --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_fragment.xml @@ -0,0 +1,34 @@ +<?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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="fill_parent" + android:layout_height="fill_parent" > + + <ProgressBar + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <com.example.android.displayingbitmaps.ui.RecyclingImageView + android:id="@+id/imageView" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:contentDescription="@string/imageview_description" /> + +</FrameLayout>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/layout/image_detail_pager.xml b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_pager.xml new file mode 100644 index 000000000..877a26bb5 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/layout/image_detail_pager.xml @@ -0,0 +1,23 @@ +<?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. +--> + +<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/pager" + android:layout_width="fill_parent" + android:layout_height="fill_parent" > + +</android.support.v4.view.ViewPager>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/layout/image_grid_fragment.xml b/samples/browseable/DisplayingBitmaps/res/layout/image_grid_fragment.xml new file mode 100644 index 000000000..e2034dea1 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/layout/image_grid_fragment.xml @@ -0,0 +1,29 @@ +<?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. +--> + +<GridView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/gridView" + style="@style/PhotoGridLayout" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:columnWidth="@dimen/image_thumbnail_size" + android:horizontalSpacing="@dimen/image_thumbnail_spacing" + android:numColumns="auto_fit" + android:stretchMode="columnWidth" + android:verticalSpacing="@dimen/image_thumbnail_spacing" > + +</GridView>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/menu/main_menu.xml b/samples/browseable/DisplayingBitmaps/res/menu/main_menu.xml new file mode 100644 index 000000000..35dad093f --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/menu/main_menu.xml @@ -0,0 +1,25 @@ +<?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. +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android" > + + <item + android:id="@+id/clear_cache" + android:icon="@android:drawable/ic_menu_delete" + android:showAsAction="never" + android:title="@string/clear_cache_menu"/> + +</menu>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values-large/dimens.xml b/samples/browseable/DisplayingBitmaps/res/values-large/dimens.xml new file mode 100644 index 000000000..c68187614 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values-large/dimens.xml @@ -0,0 +1,23 @@ +<?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> + + <dimen name="image_thumbnail_size">148dp</dimen> + <dimen name="image_thumbnail_spacing">2dp</dimen> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-dimens.xml b/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_huge</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-styles.xml b/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceLarge</item> + <item name="android:lineSpacingMultiplier">1.2</item> + <item name="android:shadowDy">-6.5</item> + </style> + +</resources> diff --git a/samples/browseable/DisplayingBitmaps/res/values-v11/styles.xml b/samples/browseable/DisplayingBitmaps/res/values-v11/styles.xml new file mode 100644 index 000000000..57da0fb6c --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values-v11/styles.xml @@ -0,0 +1,36 @@ +<?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="AppThemeDark" parent="@android:style/Theme.Holo"> + <item name="android:windowActionBarOverlay">true</item> + <item name="android:windowBackground">@android:color/black</item> + <item name="android:actionBarStyle">@style/TranslucentDarkActionBar</item> + </style> + + <style name="AppThemeDark.FullScreen" /> + + <style name="TranslucentDarkActionBar" parent="@android:style/Widget.Holo.ActionBar"> + <item name="android:background">#99000000</item> + </style> + + <!--<style name="PhotoGridLayout">--> + <!--<item name="android:drawSelectorOnTop">true</item>--> + <!--</style>--> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values-xlarge/dimens.xml b/samples/browseable/DisplayingBitmaps/res/values-xlarge/dimens.xml new file mode 100644 index 000000000..0f4397751 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values-xlarge/dimens.xml @@ -0,0 +1,23 @@ +<?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> + + <dimen name="image_thumbnail_size">198dp</dimen> + <dimen name="image_thumbnail_spacing">2dp</dimen> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values/base-strings.xml b/samples/browseable/DisplayingBitmaps/res/values/base-strings.xml new file mode 100644 index 000000000..a6a8390a6 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/base-strings.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + + + +<resources> + <string name="app_name">DisplayingBitmaps</string> + <string name="intro_message"> + <![CDATA[ + + + This is a sample application for the Android Training class + "Displaying Bitmaps Efficiently" + (http://developer.android.com/training/displaying-bitmaps/).\n\n + + It demonstrates how to load large bitmaps efficiently off the main UI thread, caching + bitmaps (both in memory and on disk), managing bitmap memory and displaying bitmaps + in UI elements such as ViewPager and ListView/GridView. + + + ]]> + </string> +</resources> diff --git a/samples/browseable/DisplayingBitmaps/res/values/colors.xml b/samples/browseable/DisplayingBitmaps/res/values/colors.xml new file mode 100644 index 000000000..521b4b944 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/colors.xml @@ -0,0 +1,23 @@ +<?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> + + <color name="grid_state_pressed">#1Affffff</color> + <color name="grid_state_focused">#80000000</color> + +</resources> diff --git a/samples/browseable/DisplayingBitmaps/res/values/dimens.xml b/samples/browseable/DisplayingBitmaps/res/values/dimens.xml new file mode 100644 index 000000000..53f61f030 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/dimens.xml @@ -0,0 +1,23 @@ +<?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> + + <dimen name="image_thumbnail_size">100dp</dimen> + <dimen name="image_thumbnail_spacing">1dp</dimen> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values/strings.xml b/samples/browseable/DisplayingBitmaps/res/values/strings.xml new file mode 100644 index 000000000..84b613785 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/strings.xml @@ -0,0 +1,25 @@ +<?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> + + <string name="clear_cache_menu">Clear Caches</string> + <string name="clear_cache_complete_toast">Caches have been cleared</string> + <string name="imageview_description">Image Thumbnail</string> + <string name="no_network_connection_toast">No network connection found</string> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values/styles.xml b/samples/browseable/DisplayingBitmaps/res/values/styles.xml new file mode 100644 index 000000000..e9aabee8f --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/styles.xml @@ -0,0 +1,29 @@ +<?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="AppThemeDark" parent="android:Theme" /> + + <style name="AppThemeDark.FullScreen" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen" /> + + <style name="PhotoGridLayout"> + <item name="android:drawSelectorOnTop">true</item> + <item name="android:listSelector">@drawable/photogrid_list_selector</item> + </style> + +</resources>
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/res/values/template-dimens.xml b/samples/browseable/DisplayingBitmaps/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/template-dimens.xml @@ -0,0 +1,32 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Define standard dimensions to comply with Holo-style grids and rhythm. --> + + <dimen name="margin_tiny">4dp</dimen> + <dimen name="margin_small">8dp</dimen> + <dimen name="margin_medium">16dp</dimen> + <dimen name="margin_large">32dp</dimen> + <dimen name="margin_huge">64dp</dimen> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml b/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml new file mode 100644 index 000000000..404623e3d --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/res/values/template-styles.xml @@ -0,0 +1,42 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Activity themes --> + + <style name="Theme.Base" parent="android:Theme.Holo.Light" /> + + <style name="Theme.Sample" parent="Theme.Base" /> + + <style name="AppTheme" parent="Theme.Sample" /> + <!-- Widget styling --> + + <style name="Widget" /> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceMedium</item> + <item name="android:lineSpacingMultiplier">1.1</item> + </style> + + <style name="Widget.SampleMessageTile"> + <item name="android:background">@drawable/tile</item> + <item name="android:shadowColor">#7F000000</item> + <item name="android:shadowDy">-3.5</item> + <item name="android:shadowRadius">2</item> + </style> + +</resources> diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/Log.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + * <p>When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.</p> + */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogFragment.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +}
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogNode.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * 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.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogView.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogWrapper.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/MessageOnlyLogFilter.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.common.logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/provider/Images.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/provider/Images.java new file mode 100644 index 000000000..5a895462d --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/provider/Images.java @@ -0,0 +1,245 @@ +/* + * 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.example.android.displayingbitmaps.provider; + +/** + * Some simple test data to use for this sample app. + */ +public class Images { + + /** + * This are PicasaWeb URLs and could potentially change. Ideally the PicasaWeb API should be + * used to fetch the URLs. + * + * Credit to Romain Guy for the photos: + * http://www.curious-creature.org/ + * https://plus.google.com/109538161516040592207/about + * http://www.flickr.com/photos/romainguy + */ + public final static String[] imageUrls = new String[] { + "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s1024/A%252520Photographer.jpg", + "https://lh4.googleusercontent.com/--dq8niRp7W4/URquVgmXvgI/AAAAAAAAAbs/-gnuLQfNnBA/s1024/A%252520Song%252520of%252520Ice%252520and%252520Fire.jpg", + "https://lh5.googleusercontent.com/-7qZeDtRKFKc/URquWZT1gOI/AAAAAAAAAbs/hqWgteyNXsg/s1024/Another%252520Rockaway%252520Sunset.jpg", + "https://lh3.googleusercontent.com/--L0Km39l5J8/URquXHGcdNI/AAAAAAAAAbs/3ZrSJNrSomQ/s1024/Antelope%252520Butte.jpg", + "https://lh6.googleusercontent.com/-8HO-4vIFnlw/URquZnsFgtI/AAAAAAAAAbs/WT8jViTF7vw/s1024/Antelope%252520Hallway.jpg", + "https://lh4.googleusercontent.com/-WIuWgVcU3Qw/URqubRVcj4I/AAAAAAAAAbs/YvbwgGjwdIQ/s1024/Antelope%252520Walls.jpg", + "https://lh6.googleusercontent.com/-UBmLbPELvoQ/URqucCdv0kI/AAAAAAAAAbs/IdNhr2VQoQs/s1024/Apre%2525CC%252580s%252520la%252520Pluie.jpg", + "https://lh3.googleusercontent.com/-s-AFpvgSeew/URquc6dF-JI/AAAAAAAAAbs/Mt3xNGRUd68/s1024/Backlit%252520Cloud.jpg", + "https://lh5.googleusercontent.com/-bvmif9a9YOQ/URquea3heHI/AAAAAAAAAbs/rcr6wyeQtAo/s1024/Bee%252520and%252520Flower.jpg", + "https://lh5.googleusercontent.com/-n7mdm7I7FGs/URqueT_BT-I/AAAAAAAAAbs/9MYmXlmpSAo/s1024/Bonzai%252520Rock%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-4CN4X4t0M1k/URqufPozWzI/AAAAAAAAAbs/8wK41lg1KPs/s1024/Caterpillar.jpg", + "https://lh3.googleusercontent.com/-rrFnVC8xQEg/URqufdrLBaI/AAAAAAAAAbs/s69WYy_fl1E/s1024/Chess.jpg", + "https://lh5.googleusercontent.com/-WVpRptWH8Yw/URqugh-QmDI/AAAAAAAAAbs/E-MgBgtlUWU/s1024/Chihuly.jpg", + "https://lh5.googleusercontent.com/-0BDXkYmckbo/URquhKFW84I/AAAAAAAAAbs/ogQtHCTk2JQ/s1024/Closed%252520Door.jpg", + "https://lh3.googleusercontent.com/-PyggXXZRykM/URquh-kVvoI/AAAAAAAAAbs/hFtDwhtrHHQ/s1024/Colorado%252520River%252520Sunset.jpg", + "https://lh3.googleusercontent.com/-ZAs4dNZtALc/URquikvOCWI/AAAAAAAAAbs/DXz4h3dll1Y/s1024/Colors%252520of%252520Autumn.jpg", + "https://lh4.googleusercontent.com/-GztnWEIiMz8/URqukVCU7bI/AAAAAAAAAbs/jo2Hjv6MZ6M/s1024/Countryside.jpg", + "https://lh4.googleusercontent.com/-bEg9EZ9QoiM/URquklz3FGI/AAAAAAAAAbs/UUuv8Ac2BaE/s1024/Death%252520Valley%252520-%252520Dunes.jpg", + "https://lh6.googleusercontent.com/-ijQJ8W68tEE/URqulGkvFEI/AAAAAAAAAbs/zPXvIwi_rFw/s1024/Delicate%252520Arch.jpg", + "https://lh5.googleusercontent.com/-Oh8mMy2ieng/URqullDwehI/AAAAAAAAAbs/TbdeEfsaIZY/s1024/Despair.jpg", + "https://lh5.googleusercontent.com/-gl0y4UiAOlk/URqumC_KjBI/AAAAAAAAAbs/PM1eT7dn4oo/s1024/Eagle%252520Fall%252520Sunrise.jpg", + "https://lh3.googleusercontent.com/-hYYHd2_vXPQ/URqumtJa9eI/AAAAAAAAAbs/wAalXVkbSh0/s1024/Electric%252520Storm.jpg", + "https://lh5.googleusercontent.com/-PyY_yiyjPTo/URqunUOhHFI/AAAAAAAAAbs/azZoULNuJXc/s1024/False%252520Kiva.jpg", + "https://lh6.googleusercontent.com/-PYvLVdvXywk/URqunwd8hfI/AAAAAAAAAbs/qiMwgkFvf6I/s1024/Fitzgerald%252520Streaks.jpg", + "https://lh4.googleusercontent.com/-KIR_UobIIqY/URquoCZ9SlI/AAAAAAAAAbs/Y4d4q8sXu4c/s1024/Foggy%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-9lzOk_OWZH0/URquoo4xYoI/AAAAAAAAAbs/AwgzHtNVCwU/s1024/Frantic.jpg", + "https://lh3.googleusercontent.com/-0X3JNaKaz48/URqupH78wpI/AAAAAAAAAbs/lHXxu_zbH8s/s1024/Golden%252520Gate%252520Afternoon.jpg", + "https://lh6.googleusercontent.com/-95sb5ag7ABc/URqupl95RDI/AAAAAAAAAbs/g73R20iVTRA/s1024/Golden%252520Gate%252520Fog.jpg", + "https://lh3.googleusercontent.com/-JB9v6rtgHhk/URqup21F-zI/AAAAAAAAAbs/64Fb8qMZWXk/s1024/Golden%252520Grass.jpg", + "https://lh4.googleusercontent.com/-EIBGfnuLtII/URquqVHwaRI/AAAAAAAAAbs/FA4McV2u8VE/s1024/Grand%252520Teton.jpg", + "https://lh4.googleusercontent.com/-WoMxZvmN9nY/URquq1v2AoI/AAAAAAAAAbs/grj5uMhL6NA/s1024/Grass%252520Closeup.jpg", + "https://lh3.googleusercontent.com/-6hZiEHXx64Q/URqurxvNdqI/AAAAAAAAAbs/kWMXM3o5OVI/s1024/Green%252520Grass.jpg", + "https://lh5.googleusercontent.com/-6LVb9OXtQ60/URquteBFuKI/AAAAAAAAAbs/4F4kRgecwFs/s1024/Hanging%252520Leaf.jpg", + "https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s1024/Highway%2525201.jpg", + "https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s1024/Horseshoe%252520Bend%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-JhFi4fb_Pqw/URquuX-QXbI/AAAAAAAAAbs/IXpYUxuweYM/s1024/Horseshoe%252520Bend.jpg", + "https://lh5.googleusercontent.com/-UGgssvFRJ7g/URquueyJzGI/AAAAAAAAAbs/yYIBlLT0toM/s1024/Into%252520the%252520Blue.jpg", + "https://lh3.googleusercontent.com/-CH7KoupI7uI/URquu0FF__I/AAAAAAAAAbs/R7GDmI7v_G0/s1024/Jelly%252520Fish%2525202.jpg", + "https://lh4.googleusercontent.com/-pwuuw6yhg8U/URquvPxR3FI/AAAAAAAAAbs/VNGk6f-tsGE/s1024/Jelly%252520Fish%2525203.jpg", + "https://lh5.googleusercontent.com/-GoUQVw1fnFw/URquv6xbC0I/AAAAAAAAAbs/zEUVTQQ43Zc/s1024/Kauai.jpg", + "https://lh6.googleusercontent.com/-8QdYYQEpYjw/URquwvdh88I/AAAAAAAAAbs/cktDy-ysfHo/s1024/Kyoto%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-vPeekyDjOE0/URquwzJ28qI/AAAAAAAAAbs/qxcyXULsZrg/s1024/Lake%252520Tahoe%252520Colors.jpg", + "https://lh4.googleusercontent.com/-xBPxWpD4yxU/URquxWHk8AI/AAAAAAAAAbs/ARDPeDYPiMY/s1024/Lava%252520from%252520the%252520Sky.jpg", + "https://lh3.googleusercontent.com/-897VXrJB6RE/URquxxxd-5I/AAAAAAAAAbs/j-Cz4T4YvIw/s1024/Leica%25252050mm%252520Summilux.jpg", + "https://lh5.googleusercontent.com/-qSJ4D4iXzGo/URquyDWiJ1I/AAAAAAAAAbs/k2pBXeWehOA/s1024/Leica%25252050mm%252520Summilux.jpg", + "https://lh6.googleusercontent.com/-dwlPg83vzLg/URquylTVuFI/AAAAAAAAAbs/G6SyQ8b4YsI/s1024/Leica%252520M8%252520%252528Front%252529.jpg", + "https://lh3.googleusercontent.com/-R3_EYAyJvfk/URquzQBv8eI/AAAAAAAAAbs/b9xhpUM3pEI/s1024/Light%252520to%252520Sand.jpg", + "https://lh3.googleusercontent.com/-fHY5h67QPi0/URqu0Cp4J1I/AAAAAAAAAbs/0lG6m94Z6vM/s1024/Little%252520Bit%252520of%252520Paradise.jpg", + "https://lh5.googleusercontent.com/-TzF_LwrCnRM/URqu0RddPOI/AAAAAAAAAbs/gaj2dLiuX0s/s1024/Lone%252520Pine%252520Sunset.jpg", + "https://lh3.googleusercontent.com/-4HdpJ4_DXU4/URqu046dJ9I/AAAAAAAAAbs/eBOodtk2_uk/s1024/Lonely%252520Rock.jpg", + "https://lh6.googleusercontent.com/-erbF--z-W4s/URqu1ajSLkI/AAAAAAAAAbs/xjDCDO1INzM/s1024/Longue%252520Vue.jpg", + "https://lh6.googleusercontent.com/-0CXJRdJaqvc/URqu1opNZNI/AAAAAAAAAbs/PFB2oPUU7Lk/s1024/Look%252520Me%252520in%252520the%252520Eye.jpg", + "https://lh3.googleusercontent.com/-D_5lNxnDN6g/URqu2Tk7HVI/AAAAAAAAAbs/p0ddca9W__Y/s1024/Lost%252520in%252520a%252520Field.jpg", + "https://lh6.googleusercontent.com/-flsqwMrIk2Q/URqu24PcmjI/AAAAAAAAAbs/5ocIH85XofM/s1024/Marshall%252520Beach%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-Y4lgryEVTmU/URqu28kG3gI/AAAAAAAAAbs/OjXpekqtbJ4/s1024/Mono%252520Lake%252520Blue.jpg", + "https://lh4.googleusercontent.com/-AaHAJPmcGYA/URqu3PIldHI/AAAAAAAAAbs/lcTqk1SIcRs/s1024/Monument%252520Valley%252520Overlook.jpg", + "https://lh4.googleusercontent.com/-vKxfdQ83dQA/URqu31Yq_BI/AAAAAAAAAbs/OUoGk_2AyfM/s1024/Moving%252520Rock.jpg", + "https://lh5.googleusercontent.com/-CG62QiPpWXg/URqu4ia4vRI/AAAAAAAAAbs/0YOdqLAlcAc/s1024/Napali%252520Coast.jpg", + "https://lh6.googleusercontent.com/-wdGrP5PMmJQ/URqu5PZvn7I/AAAAAAAAAbs/m0abEcdPXe4/s1024/One%252520Wheel.jpg", + "https://lh6.googleusercontent.com/-6WS5DoCGuOA/URqu5qx1UgI/AAAAAAAAAbs/giMw2ixPvrY/s1024/Open%252520Sky.jpg", + "https://lh6.googleusercontent.com/-u8EHKj8G8GQ/URqu55sM6yI/AAAAAAAAAbs/lIXX_GlTdmI/s1024/Orange%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-74Z5qj4bTDE/URqu6LSrJrI/AAAAAAAAAbs/XzmVkw90szQ/s1024/Orchid.jpg", + "https://lh6.googleusercontent.com/-lEQE4h6TePE/URqu6t_lSkI/AAAAAAAAAbs/zvGYKOea_qY/s1024/Over%252520there.jpg", + "https://lh5.googleusercontent.com/-cauH-53JH2M/URqu66v_USI/AAAAAAAAAbs/EucwwqclfKQ/s1024/Plumes.jpg", + "https://lh3.googleusercontent.com/-eDLT2jHDoy4/URqu7axzkAI/AAAAAAAAAbs/iVZE-xJ7lZs/s1024/Rainbokeh.jpg", + "https://lh5.googleusercontent.com/-j1NLqEFIyco/URqu8L1CGcI/AAAAAAAAAbs/aqZkgX66zlI/s1024/Rainbow.jpg", + "https://lh5.googleusercontent.com/-DRnqmK0t4VU/URqu8XYN9yI/AAAAAAAAAbs/LgvF_592WLU/s1024/Rice%252520Fields.jpg", + "https://lh3.googleusercontent.com/-hwh1v3EOGcQ/URqu8qOaKwI/AAAAAAAAAbs/IljRJRnbJGw/s1024/Rockaway%252520Fire%252520Sky.jpg", + "https://lh5.googleusercontent.com/-wjV6FQk7tlk/URqu9jCQ8sI/AAAAAAAAAbs/RyYUpdo-c9o/s1024/Rockaway%252520Flow.jpg", + "https://lh6.googleusercontent.com/-6cAXNfo7D20/URqu-BdzgPI/AAAAAAAAAbs/OmsYllzJqwo/s1024/Rockaway%252520Sunset%252520Sky.jpg", + "https://lh3.googleusercontent.com/-sl8fpGPS-RE/URqu_BOkfgI/AAAAAAAAAbs/Dg2Fv-JxOeg/s1024/Russian%252520Ridge%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-gVtY36mMBIg/URqu_q91lkI/AAAAAAAAAbs/3CiFMBcy5MA/s1024/Rust%252520Knot.jpg", + "https://lh6.googleusercontent.com/-GHeImuHqJBE/URqu_FKfVLI/AAAAAAAAAbs/axuEJeqam7Q/s1024/Sailing%252520Stones.jpg", + "https://lh3.googleusercontent.com/-hBbYZjTOwGc/URqu_ycpIrI/AAAAAAAAAbs/nAdJUXnGJYE/s1024/Seahorse.jpg", + "https://lh3.googleusercontent.com/-Iwi6-i6IexY/URqvAYZHsVI/AAAAAAAAAbs/5ETWl4qXsFE/s1024/Shinjuku%252520Street.jpg", + "https://lh6.googleusercontent.com/-amhnySTM_MY/URqvAlb5KoI/AAAAAAAAAbs/pFCFgzlKsn0/s1024/Sierra%252520Heavens.jpg", + "https://lh5.googleusercontent.com/-dJgjepFrYSo/URqvBVJZrAI/AAAAAAAAAbs/v-F5QWpYO6s/s1024/Sierra%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-Z4zGiC5nWdc/URqvBdEwivI/AAAAAAAAAbs/ZRZR1VJ84QA/s1024/Sin%252520Lights.jpg", + "https://lh4.googleusercontent.com/-_0cYiWW8ccY/URqvBz3iM4I/AAAAAAAAAbs/9N_Wq8MhLTY/s1024/Starry%252520Lake.jpg", + "https://lh3.googleusercontent.com/-A9LMoRyuQUA/URqvCYx_JoI/AAAAAAAAAbs/s7sde1Bz9cI/s1024/Starry%252520Night.jpg", + "https://lh3.googleusercontent.com/-KtLJ3k858eY/URqvC_2h_bI/AAAAAAAAAbs/zzEBImwDA_g/s1024/Stream.jpg", + "https://lh5.googleusercontent.com/-dFB7Lad6RcA/URqvDUftwWI/AAAAAAAAAbs/BrhoUtXTN7o/s1024/Strip%252520Sunset.jpg", + "https://lh5.googleusercontent.com/-at6apgFiN20/URqvDyffUZI/AAAAAAAAAbs/clABCx171bE/s1024/Sunset%252520Hills.jpg", + "https://lh4.googleusercontent.com/-7-EHhtQthII/URqvEYTk4vI/AAAAAAAAAbs/QSJZoB3YjVg/s1024/Tenaya%252520Lake%2525202.jpg", + "https://lh6.googleusercontent.com/-8MrjV_a-Pok/URqvFC5repI/AAAAAAAAAbs/9inKTg9fbCE/s1024/Tenaya%252520Lake.jpg", + "https://lh5.googleusercontent.com/-B1HW-z4zwao/URqvFWYRwUI/AAAAAAAAAbs/8Peli53Bs8I/s1024/The%252520Cave%252520BW.jpg", + "https://lh3.googleusercontent.com/-PO4E-xZKAnQ/URqvGRqjYkI/AAAAAAAAAbs/42nyADFsXag/s1024/The%252520Fisherman.jpg", + "https://lh4.googleusercontent.com/-iLyZlzfdy7s/URqvG0YScdI/AAAAAAAAAbs/1J9eDKmkXtk/s1024/The%252520Night%252520is%252520Coming.jpg", + "https://lh6.googleusercontent.com/-G-k7YkkUco0/URqvHhah6fI/AAAAAAAAAbs/_taQQG7t0vo/s1024/The%252520Road.jpg", + "https://lh6.googleusercontent.com/-h-ALJt7kSus/URqvIThqYfI/AAAAAAAAAbs/ejiv35olWS8/s1024/Tokyo%252520Heights.jpg", + "https://lh5.googleusercontent.com/-Hy9k-TbS7xg/URqvIjQMOxI/AAAAAAAAAbs/RSpmmOATSkg/s1024/Tokyo%252520Highway.jpg", + "https://lh6.googleusercontent.com/-83oOvMb4OZs/URqvJL0T7lI/AAAAAAAAAbs/c5TECZ6RONM/s1024/Tokyo%252520Smog.jpg", + "https://lh3.googleusercontent.com/-FB-jfgREEfI/URqvJI3EXAI/AAAAAAAAAbs/XfyweiRF4v8/s1024/Tufa%252520at%252520Night.jpg", + "https://lh4.googleusercontent.com/-vngKD5Z1U8w/URqvJUCEgPI/AAAAAAAAAbs/ulxCMVcU6EU/s1024/Valley%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-DOz5I2E2oMQ/URqvKMND1kI/AAAAAAAAAbs/Iqf0IsInleo/s1024/Windmill%252520Sunrise.jpg", + "https://lh5.googleusercontent.com/-biyiyWcJ9MU/URqvKculiAI/AAAAAAAAAbs/jyPsCplJOpE/s1024/Windmill.jpg", + "https://lh4.googleusercontent.com/-PDT167_xRdA/URqvK36mLcI/AAAAAAAAAbs/oi2ik9QseMI/s1024/Windmills.jpg", + "https://lh5.googleusercontent.com/-kI_QdYx7VlU/URqvLXCB6gI/AAAAAAAAAbs/N31vlZ6u89o/s1024/Yet%252520Another%252520Rockaway%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-e9NHZ5k5MSs/URqvMIBZjtI/AAAAAAAAAbs/1fV810rDNfQ/s1024/Yosemite%252520Tree.jpg", + }; + + /** + * This are PicasaWeb thumbnail URLs and could potentially change. Ideally the PicasaWeb API + * should be used to fetch the URLs. + * + * Credit to Romain Guy for the photos: + * http://www.curious-creature.org/ + * https://plus.google.com/109538161516040592207/about + * http://www.flickr.com/photos/romainguy + */ + public final static String[] imageThumbUrls = new String[] { + "https://lh6.googleusercontent.com/-55osAWw3x0Q/URquUtcFr5I/AAAAAAAAAbs/rWlj1RUKrYI/s240-c/A%252520Photographer.jpg", + "https://lh4.googleusercontent.com/--dq8niRp7W4/URquVgmXvgI/AAAAAAAAAbs/-gnuLQfNnBA/s240-c/A%252520Song%252520of%252520Ice%252520and%252520Fire.jpg", + "https://lh5.googleusercontent.com/-7qZeDtRKFKc/URquWZT1gOI/AAAAAAAAAbs/hqWgteyNXsg/s240-c/Another%252520Rockaway%252520Sunset.jpg", + "https://lh3.googleusercontent.com/--L0Km39l5J8/URquXHGcdNI/AAAAAAAAAbs/3ZrSJNrSomQ/s240-c/Antelope%252520Butte.jpg", + "https://lh6.googleusercontent.com/-8HO-4vIFnlw/URquZnsFgtI/AAAAAAAAAbs/WT8jViTF7vw/s240-c/Antelope%252520Hallway.jpg", + "https://lh4.googleusercontent.com/-WIuWgVcU3Qw/URqubRVcj4I/AAAAAAAAAbs/YvbwgGjwdIQ/s240-c/Antelope%252520Walls.jpg", + "https://lh6.googleusercontent.com/-UBmLbPELvoQ/URqucCdv0kI/AAAAAAAAAbs/IdNhr2VQoQs/s240-c/Apre%2525CC%252580s%252520la%252520Pluie.jpg", + "https://lh3.googleusercontent.com/-s-AFpvgSeew/URquc6dF-JI/AAAAAAAAAbs/Mt3xNGRUd68/s240-c/Backlit%252520Cloud.jpg", + "https://lh5.googleusercontent.com/-bvmif9a9YOQ/URquea3heHI/AAAAAAAAAbs/rcr6wyeQtAo/s240-c/Bee%252520and%252520Flower.jpg", + "https://lh5.googleusercontent.com/-n7mdm7I7FGs/URqueT_BT-I/AAAAAAAAAbs/9MYmXlmpSAo/s240-c/Bonzai%252520Rock%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-4CN4X4t0M1k/URqufPozWzI/AAAAAAAAAbs/8wK41lg1KPs/s240-c/Caterpillar.jpg", + "https://lh3.googleusercontent.com/-rrFnVC8xQEg/URqufdrLBaI/AAAAAAAAAbs/s69WYy_fl1E/s240-c/Chess.jpg", + "https://lh5.googleusercontent.com/-WVpRptWH8Yw/URqugh-QmDI/AAAAAAAAAbs/E-MgBgtlUWU/s240-c/Chihuly.jpg", + "https://lh5.googleusercontent.com/-0BDXkYmckbo/URquhKFW84I/AAAAAAAAAbs/ogQtHCTk2JQ/s240-c/Closed%252520Door.jpg", + "https://lh3.googleusercontent.com/-PyggXXZRykM/URquh-kVvoI/AAAAAAAAAbs/hFtDwhtrHHQ/s240-c/Colorado%252520River%252520Sunset.jpg", + "https://lh3.googleusercontent.com/-ZAs4dNZtALc/URquikvOCWI/AAAAAAAAAbs/DXz4h3dll1Y/s240-c/Colors%252520of%252520Autumn.jpg", + "https://lh4.googleusercontent.com/-GztnWEIiMz8/URqukVCU7bI/AAAAAAAAAbs/jo2Hjv6MZ6M/s240-c/Countryside.jpg", + "https://lh4.googleusercontent.com/-bEg9EZ9QoiM/URquklz3FGI/AAAAAAAAAbs/UUuv8Ac2BaE/s240-c/Death%252520Valley%252520-%252520Dunes.jpg", + "https://lh6.googleusercontent.com/-ijQJ8W68tEE/URqulGkvFEI/AAAAAAAAAbs/zPXvIwi_rFw/s240-c/Delicate%252520Arch.jpg", + "https://lh5.googleusercontent.com/-Oh8mMy2ieng/URqullDwehI/AAAAAAAAAbs/TbdeEfsaIZY/s240-c/Despair.jpg", + "https://lh5.googleusercontent.com/-gl0y4UiAOlk/URqumC_KjBI/AAAAAAAAAbs/PM1eT7dn4oo/s240-c/Eagle%252520Fall%252520Sunrise.jpg", + "https://lh3.googleusercontent.com/-hYYHd2_vXPQ/URqumtJa9eI/AAAAAAAAAbs/wAalXVkbSh0/s240-c/Electric%252520Storm.jpg", + "https://lh5.googleusercontent.com/-PyY_yiyjPTo/URqunUOhHFI/AAAAAAAAAbs/azZoULNuJXc/s240-c/False%252520Kiva.jpg", + "https://lh6.googleusercontent.com/-PYvLVdvXywk/URqunwd8hfI/AAAAAAAAAbs/qiMwgkFvf6I/s240-c/Fitzgerald%252520Streaks.jpg", + "https://lh4.googleusercontent.com/-KIR_UobIIqY/URquoCZ9SlI/AAAAAAAAAbs/Y4d4q8sXu4c/s240-c/Foggy%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-9lzOk_OWZH0/URquoo4xYoI/AAAAAAAAAbs/AwgzHtNVCwU/s240-c/Frantic.jpg", + "https://lh3.googleusercontent.com/-0X3JNaKaz48/URqupH78wpI/AAAAAAAAAbs/lHXxu_zbH8s/s240-c/Golden%252520Gate%252520Afternoon.jpg", + "https://lh6.googleusercontent.com/-95sb5ag7ABc/URqupl95RDI/AAAAAAAAAbs/g73R20iVTRA/s240-c/Golden%252520Gate%252520Fog.jpg", + "https://lh3.googleusercontent.com/-JB9v6rtgHhk/URqup21F-zI/AAAAAAAAAbs/64Fb8qMZWXk/s240-c/Golden%252520Grass.jpg", + "https://lh4.googleusercontent.com/-EIBGfnuLtII/URquqVHwaRI/AAAAAAAAAbs/FA4McV2u8VE/s240-c/Grand%252520Teton.jpg", + "https://lh4.googleusercontent.com/-WoMxZvmN9nY/URquq1v2AoI/AAAAAAAAAbs/grj5uMhL6NA/s240-c/Grass%252520Closeup.jpg", + "https://lh3.googleusercontent.com/-6hZiEHXx64Q/URqurxvNdqI/AAAAAAAAAbs/kWMXM3o5OVI/s240-c/Green%252520Grass.jpg", + "https://lh5.googleusercontent.com/-6LVb9OXtQ60/URquteBFuKI/AAAAAAAAAbs/4F4kRgecwFs/s240-c/Hanging%252520Leaf.jpg", + "https://lh4.googleusercontent.com/-zAvf__52ONk/URqutT_IuxI/AAAAAAAAAbs/D_bcuc0thoU/s240-c/Highway%2525201.jpg", + "https://lh6.googleusercontent.com/-H4SrUg615rA/URquuL27fXI/AAAAAAAAAbs/4aEqJfiMsOU/s240-c/Horseshoe%252520Bend%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-JhFi4fb_Pqw/URquuX-QXbI/AAAAAAAAAbs/IXpYUxuweYM/s240-c/Horseshoe%252520Bend.jpg", + "https://lh5.googleusercontent.com/-UGgssvFRJ7g/URquueyJzGI/AAAAAAAAAbs/yYIBlLT0toM/s240-c/Into%252520the%252520Blue.jpg", + "https://lh3.googleusercontent.com/-CH7KoupI7uI/URquu0FF__I/AAAAAAAAAbs/R7GDmI7v_G0/s240-c/Jelly%252520Fish%2525202.jpg", + "https://lh4.googleusercontent.com/-pwuuw6yhg8U/URquvPxR3FI/AAAAAAAAAbs/VNGk6f-tsGE/s240-c/Jelly%252520Fish%2525203.jpg", + "https://lh5.googleusercontent.com/-GoUQVw1fnFw/URquv6xbC0I/AAAAAAAAAbs/zEUVTQQ43Zc/s240-c/Kauai.jpg", + "https://lh6.googleusercontent.com/-8QdYYQEpYjw/URquwvdh88I/AAAAAAAAAbs/cktDy-ysfHo/s240-c/Kyoto%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-vPeekyDjOE0/URquwzJ28qI/AAAAAAAAAbs/qxcyXULsZrg/s240-c/Lake%252520Tahoe%252520Colors.jpg", + "https://lh4.googleusercontent.com/-xBPxWpD4yxU/URquxWHk8AI/AAAAAAAAAbs/ARDPeDYPiMY/s240-c/Lava%252520from%252520the%252520Sky.jpg", + "https://lh3.googleusercontent.com/-897VXrJB6RE/URquxxxd-5I/AAAAAAAAAbs/j-Cz4T4YvIw/s240-c/Leica%25252050mm%252520Summilux.jpg", + "https://lh5.googleusercontent.com/-qSJ4D4iXzGo/URquyDWiJ1I/AAAAAAAAAbs/k2pBXeWehOA/s240-c/Leica%25252050mm%252520Summilux.jpg", + "https://lh6.googleusercontent.com/-dwlPg83vzLg/URquylTVuFI/AAAAAAAAAbs/G6SyQ8b4YsI/s240-c/Leica%252520M8%252520%252528Front%252529.jpg", + "https://lh3.googleusercontent.com/-R3_EYAyJvfk/URquzQBv8eI/AAAAAAAAAbs/b9xhpUM3pEI/s240-c/Light%252520to%252520Sand.jpg", + "https://lh3.googleusercontent.com/-fHY5h67QPi0/URqu0Cp4J1I/AAAAAAAAAbs/0lG6m94Z6vM/s240-c/Little%252520Bit%252520of%252520Paradise.jpg", + "https://lh5.googleusercontent.com/-TzF_LwrCnRM/URqu0RddPOI/AAAAAAAAAbs/gaj2dLiuX0s/s240-c/Lone%252520Pine%252520Sunset.jpg", + "https://lh3.googleusercontent.com/-4HdpJ4_DXU4/URqu046dJ9I/AAAAAAAAAbs/eBOodtk2_uk/s240-c/Lonely%252520Rock.jpg", + "https://lh6.googleusercontent.com/-erbF--z-W4s/URqu1ajSLkI/AAAAAAAAAbs/xjDCDO1INzM/s240-c/Longue%252520Vue.jpg", + "https://lh6.googleusercontent.com/-0CXJRdJaqvc/URqu1opNZNI/AAAAAAAAAbs/PFB2oPUU7Lk/s240-c/Look%252520Me%252520in%252520the%252520Eye.jpg", + "https://lh3.googleusercontent.com/-D_5lNxnDN6g/URqu2Tk7HVI/AAAAAAAAAbs/p0ddca9W__Y/s240-c/Lost%252520in%252520a%252520Field.jpg", + "https://lh6.googleusercontent.com/-flsqwMrIk2Q/URqu24PcmjI/AAAAAAAAAbs/5ocIH85XofM/s240-c/Marshall%252520Beach%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-Y4lgryEVTmU/URqu28kG3gI/AAAAAAAAAbs/OjXpekqtbJ4/s240-c/Mono%252520Lake%252520Blue.jpg", + "https://lh4.googleusercontent.com/-AaHAJPmcGYA/URqu3PIldHI/AAAAAAAAAbs/lcTqk1SIcRs/s240-c/Monument%252520Valley%252520Overlook.jpg", + "https://lh4.googleusercontent.com/-vKxfdQ83dQA/URqu31Yq_BI/AAAAAAAAAbs/OUoGk_2AyfM/s240-c/Moving%252520Rock.jpg", + "https://lh5.googleusercontent.com/-CG62QiPpWXg/URqu4ia4vRI/AAAAAAAAAbs/0YOdqLAlcAc/s240-c/Napali%252520Coast.jpg", + "https://lh6.googleusercontent.com/-wdGrP5PMmJQ/URqu5PZvn7I/AAAAAAAAAbs/m0abEcdPXe4/s240-c/One%252520Wheel.jpg", + "https://lh6.googleusercontent.com/-6WS5DoCGuOA/URqu5qx1UgI/AAAAAAAAAbs/giMw2ixPvrY/s240-c/Open%252520Sky.jpg", + "https://lh6.googleusercontent.com/-u8EHKj8G8GQ/URqu55sM6yI/AAAAAAAAAbs/lIXX_GlTdmI/s240-c/Orange%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-74Z5qj4bTDE/URqu6LSrJrI/AAAAAAAAAbs/XzmVkw90szQ/s240-c/Orchid.jpg", + "https://lh6.googleusercontent.com/-lEQE4h6TePE/URqu6t_lSkI/AAAAAAAAAbs/zvGYKOea_qY/s240-c/Over%252520there.jpg", + "https://lh5.googleusercontent.com/-cauH-53JH2M/URqu66v_USI/AAAAAAAAAbs/EucwwqclfKQ/s240-c/Plumes.jpg", + "https://lh3.googleusercontent.com/-eDLT2jHDoy4/URqu7axzkAI/AAAAAAAAAbs/iVZE-xJ7lZs/s240-c/Rainbokeh.jpg", + "https://lh5.googleusercontent.com/-j1NLqEFIyco/URqu8L1CGcI/AAAAAAAAAbs/aqZkgX66zlI/s240-c/Rainbow.jpg", + "https://lh5.googleusercontent.com/-DRnqmK0t4VU/URqu8XYN9yI/AAAAAAAAAbs/LgvF_592WLU/s240-c/Rice%252520Fields.jpg", + "https://lh3.googleusercontent.com/-hwh1v3EOGcQ/URqu8qOaKwI/AAAAAAAAAbs/IljRJRnbJGw/s240-c/Rockaway%252520Fire%252520Sky.jpg", + "https://lh5.googleusercontent.com/-wjV6FQk7tlk/URqu9jCQ8sI/AAAAAAAAAbs/RyYUpdo-c9o/s240-c/Rockaway%252520Flow.jpg", + "https://lh6.googleusercontent.com/-6cAXNfo7D20/URqu-BdzgPI/AAAAAAAAAbs/OmsYllzJqwo/s240-c/Rockaway%252520Sunset%252520Sky.jpg", + "https://lh3.googleusercontent.com/-sl8fpGPS-RE/URqu_BOkfgI/AAAAAAAAAbs/Dg2Fv-JxOeg/s240-c/Russian%252520Ridge%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-gVtY36mMBIg/URqu_q91lkI/AAAAAAAAAbs/3CiFMBcy5MA/s240-c/Rust%252520Knot.jpg", + "https://lh6.googleusercontent.com/-GHeImuHqJBE/URqu_FKfVLI/AAAAAAAAAbs/axuEJeqam7Q/s240-c/Sailing%252520Stones.jpg", + "https://lh3.googleusercontent.com/-hBbYZjTOwGc/URqu_ycpIrI/AAAAAAAAAbs/nAdJUXnGJYE/s240-c/Seahorse.jpg", + "https://lh3.googleusercontent.com/-Iwi6-i6IexY/URqvAYZHsVI/AAAAAAAAAbs/5ETWl4qXsFE/s240-c/Shinjuku%252520Street.jpg", + "https://lh6.googleusercontent.com/-amhnySTM_MY/URqvAlb5KoI/AAAAAAAAAbs/pFCFgzlKsn0/s240-c/Sierra%252520Heavens.jpg", + "https://lh5.googleusercontent.com/-dJgjepFrYSo/URqvBVJZrAI/AAAAAAAAAbs/v-F5QWpYO6s/s240-c/Sierra%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-Z4zGiC5nWdc/URqvBdEwivI/AAAAAAAAAbs/ZRZR1VJ84QA/s240-c/Sin%252520Lights.jpg", + "https://lh4.googleusercontent.com/-_0cYiWW8ccY/URqvBz3iM4I/AAAAAAAAAbs/9N_Wq8MhLTY/s240-c/Starry%252520Lake.jpg", + "https://lh3.googleusercontent.com/-A9LMoRyuQUA/URqvCYx_JoI/AAAAAAAAAbs/s7sde1Bz9cI/s240-c/Starry%252520Night.jpg", + "https://lh3.googleusercontent.com/-KtLJ3k858eY/URqvC_2h_bI/AAAAAAAAAbs/zzEBImwDA_g/s240-c/Stream.jpg", + "https://lh5.googleusercontent.com/-dFB7Lad6RcA/URqvDUftwWI/AAAAAAAAAbs/BrhoUtXTN7o/s240-c/Strip%252520Sunset.jpg", + "https://lh5.googleusercontent.com/-at6apgFiN20/URqvDyffUZI/AAAAAAAAAbs/clABCx171bE/s240-c/Sunset%252520Hills.jpg", + "https://lh4.googleusercontent.com/-7-EHhtQthII/URqvEYTk4vI/AAAAAAAAAbs/QSJZoB3YjVg/s240-c/Tenaya%252520Lake%2525202.jpg", + "https://lh6.googleusercontent.com/-8MrjV_a-Pok/URqvFC5repI/AAAAAAAAAbs/9inKTg9fbCE/s240-c/Tenaya%252520Lake.jpg", + "https://lh5.googleusercontent.com/-B1HW-z4zwao/URqvFWYRwUI/AAAAAAAAAbs/8Peli53Bs8I/s240-c/The%252520Cave%252520BW.jpg", + "https://lh3.googleusercontent.com/-PO4E-xZKAnQ/URqvGRqjYkI/AAAAAAAAAbs/42nyADFsXag/s240-c/The%252520Fisherman.jpg", + "https://lh4.googleusercontent.com/-iLyZlzfdy7s/URqvG0YScdI/AAAAAAAAAbs/1J9eDKmkXtk/s240-c/The%252520Night%252520is%252520Coming.jpg", + "https://lh6.googleusercontent.com/-G-k7YkkUco0/URqvHhah6fI/AAAAAAAAAbs/_taQQG7t0vo/s240-c/The%252520Road.jpg", + "https://lh6.googleusercontent.com/-h-ALJt7kSus/URqvIThqYfI/AAAAAAAAAbs/ejiv35olWS8/s240-c/Tokyo%252520Heights.jpg", + "https://lh5.googleusercontent.com/-Hy9k-TbS7xg/URqvIjQMOxI/AAAAAAAAAbs/RSpmmOATSkg/s240-c/Tokyo%252520Highway.jpg", + "https://lh6.googleusercontent.com/-83oOvMb4OZs/URqvJL0T7lI/AAAAAAAAAbs/c5TECZ6RONM/s240-c/Tokyo%252520Smog.jpg", + "https://lh3.googleusercontent.com/-FB-jfgREEfI/URqvJI3EXAI/AAAAAAAAAbs/XfyweiRF4v8/s240-c/Tufa%252520at%252520Night.jpg", + "https://lh4.googleusercontent.com/-vngKD5Z1U8w/URqvJUCEgPI/AAAAAAAAAbs/ulxCMVcU6EU/s240-c/Valley%252520Sunset.jpg", + "https://lh6.googleusercontent.com/-DOz5I2E2oMQ/URqvKMND1kI/AAAAAAAAAbs/Iqf0IsInleo/s240-c/Windmill%252520Sunrise.jpg", + "https://lh5.googleusercontent.com/-biyiyWcJ9MU/URqvKculiAI/AAAAAAAAAbs/jyPsCplJOpE/s240-c/Windmill.jpg", + "https://lh4.googleusercontent.com/-PDT167_xRdA/URqvK36mLcI/AAAAAAAAAbs/oi2ik9QseMI/s240-c/Windmills.jpg", + "https://lh5.googleusercontent.com/-kI_QdYx7VlU/URqvLXCB6gI/AAAAAAAAAbs/N31vlZ6u89o/s240-c/Yet%252520Another%252520Rockaway%252520Sunset.jpg", + "https://lh4.googleusercontent.com/-e9NHZ5k5MSs/URqvMIBZjtI/AAAAAAAAAbs/1fV810rDNfQ/s240-c/Yosemite%252520Tree.jpg", + }; +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailActivity.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailActivity.java new file mode 100644 index 000000000..c2be1acfc --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailActivity.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.example.android.displayingbitmaps.ui; + +import android.annotation.TargetApi; +import android.app.ActionBar; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.NavUtils; +import android.support.v4.view.ViewPager; +import android.util.DisplayMetrics; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager.LayoutParams; +import android.widget.Toast; + +import com.example.android.displayingbitmaps.BuildConfig; +import com.example.android.displayingbitmaps.R; +import com.example.android.displayingbitmaps.provider.Images; +import com.example.android.displayingbitmaps.util.ImageCache; +import com.example.android.displayingbitmaps.util.ImageFetcher; +import com.example.android.displayingbitmaps.util.Utils; + +public class ImageDetailActivity extends FragmentActivity implements OnClickListener { + private static final String IMAGE_CACHE_DIR = "images"; + public static final String EXTRA_IMAGE = "extra_image"; + + private ImagePagerAdapter mAdapter; + private ImageFetcher mImageFetcher; + private ViewPager mPager; + + @TargetApi(VERSION_CODES.HONEYCOMB) + @Override + public void onCreate(Bundle savedInstanceState) { + if (BuildConfig.DEBUG) { + Utils.enableStrictMode(); + } + super.onCreate(savedInstanceState); + setContentView(R.layout.image_detail_pager); + + // Fetch screen height and width, to use as our max size when loading images as this + // activity runs full screen + final DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + final int height = displayMetrics.heightPixels; + final int width = displayMetrics.widthPixels; + + // For this sample we'll use half of the longest width to resize our images. As the + // image scaling ensures the image is larger than this, we should be left with a + // resolution that is appropriate for both portrait and landscape. For best image quality + // we shouldn't divide by 2, but this will use more memory and require a larger memory + // cache. + final int longest = (height > width ? height : width) / 2; + + ImageCache.ImageCacheParams cacheParams = + new ImageCache.ImageCacheParams(this, IMAGE_CACHE_DIR); + cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory + + // The ImageFetcher takes care of loading images into our ImageView children asynchronously + mImageFetcher = new ImageFetcher(this, longest); + mImageFetcher.addImageCache(getSupportFragmentManager(), cacheParams); + mImageFetcher.setImageFadeIn(false); + + // Set up ViewPager and backing adapter + mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), Images.imageUrls.length); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mAdapter); + mPager.setPageMargin((int) getResources().getDimension(R.dimen.horizontal_page_margin)); + mPager.setOffscreenPageLimit(2); + + // Set up activity to go full screen + getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN); + + // Enable some additional newer visibility and ActionBar features to create a more + // immersive photo viewing experience + if (Utils.hasHoneycomb()) { + final ActionBar actionBar = getActionBar(); + + // Hide title text and set home as up + actionBar.setDisplayShowTitleEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + + // Hide and show the ActionBar as the visibility changes + mPager.setOnSystemUiVisibilityChangeListener( + new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int vis) { + if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { + actionBar.hide(); + } else { + actionBar.show(); + } + } + }); + + // Start low profile mode and hide ActionBar + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + actionBar.hide(); + } + + // Set the current item based on the extra passed in to this activity + final int extraCurrentItem = getIntent().getIntExtra(EXTRA_IMAGE, -1); + if (extraCurrentItem != -1) { + mPager.setCurrentItem(extraCurrentItem); + } + } + + @Override + public void onResume() { + super.onResume(); + mImageFetcher.setExitTasksEarly(false); + } + + @Override + protected void onPause() { + super.onPause(); + mImageFetcher.setExitTasksEarly(true); + mImageFetcher.flushCache(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mImageFetcher.closeCache(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + case R.id.clear_cache: + mImageFetcher.clearCache(); + Toast.makeText( + this, R.string.clear_cache_complete_toast,Toast.LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; + } + + /** + * Called by the ViewPager child fragments to load images via the one ImageFetcher + */ + public ImageFetcher getImageFetcher() { + return mImageFetcher; + } + + /** + * The main adapter that backs the ViewPager. A subclass of FragmentStatePagerAdapter as there + * could be a large number of items in the ViewPager and we don't want to retain them all in + * memory at once but create/destroy them on the fly. + */ + private class ImagePagerAdapter extends FragmentStatePagerAdapter { + private final int mSize; + + public ImagePagerAdapter(FragmentManager fm, int size) { + super(fm); + mSize = size; + } + + @Override + public int getCount() { + return mSize; + } + + @Override + public Fragment getItem(int position) { + return ImageDetailFragment.newInstance(Images.imageUrls[position]); + } + } + + /** + * Set on the ImageView in the ViewPager children fragments, to enable/disable low profile mode + * when the ImageView is touched. + */ + @TargetApi(VERSION_CODES.HONEYCOMB) + @Override + public void onClick(View v) { + final int vis = mPager.getSystemUiVisibility(); + if ((vis & View.SYSTEM_UI_FLAG_LOW_PROFILE) != 0) { + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); + } else { + mPager.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java new file mode 100644 index 000000000..506729a7e --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageDetailFragment.java @@ -0,0 +1,107 @@ +/* + * 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.example.android.displayingbitmaps.ui; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.ImageView; + +import com.example.android.displayingbitmaps.R; +import com.example.android.displayingbitmaps.util.ImageFetcher; +import com.example.android.displayingbitmaps.util.ImageWorker; +import com.example.android.displayingbitmaps.util.Utils; + +/** + * This fragment will populate the children of the ViewPager from {@link ImageDetailActivity}. + */ +public class ImageDetailFragment extends Fragment { + private static final String IMAGE_DATA_EXTRA = "extra_image_data"; + private String mImageUrl; + private ImageView mImageView; + private ImageFetcher mImageFetcher; + + /** + * Factory method to generate a new instance of the fragment given an image number. + * + * @param imageUrl The image url to load + * @return A new instance of ImageDetailFragment with imageNum extras + */ + public static ImageDetailFragment newInstance(String imageUrl) { + final ImageDetailFragment f = new ImageDetailFragment(); + + final Bundle args = new Bundle(); + args.putString(IMAGE_DATA_EXTRA, imageUrl); + f.setArguments(args); + + return f; + } + + /** + * Empty constructor as per the Fragment documentation + */ + public ImageDetailFragment() {} + + /** + * Populate image using a url from extras, use the convenience factory method + * {@link ImageDetailFragment#newInstance(String)} to create this fragment. + */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mImageUrl = getArguments() != null ? getArguments().getString(IMAGE_DATA_EXTRA) : null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + // Inflate and locate the main ImageView + final View v = inflater.inflate(R.layout.image_detail_fragment, container, false); + mImageView = (ImageView) v.findViewById(R.id.imageView); + return v; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Use the parent activity to load the image asynchronously into the ImageView (so a single + // cache can be used over all pages in the ViewPager + if (ImageDetailActivity.class.isInstance(getActivity())) { + mImageFetcher = ((ImageDetailActivity) getActivity()).getImageFetcher(); + mImageFetcher.loadImage(mImageUrl, mImageView); + } + + // Pass clicks on the ImageView to the parent activity to handle + if (OnClickListener.class.isInstance(getActivity()) && Utils.hasHoneycomb()) { + mImageView.setOnClickListener((OnClickListener) getActivity()); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mImageView != null) { + // Cancel any pending image work + ImageWorker.cancelWork(mImageView); + mImageView.setImageDrawable(null); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridActivity.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridActivity.java new file mode 100644 index 000000000..f171955ae --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridActivity.java @@ -0,0 +1,45 @@ +/* + * 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.example.android.displayingbitmaps.ui; + +import android.os.Bundle; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentTransaction; + +import com.example.android.displayingbitmaps.BuildConfig; +import com.example.android.displayingbitmaps.util.Utils; + +/** + * Simple FragmentActivity to hold the main {@link ImageGridFragment} and not much else. + */ +public class ImageGridActivity extends FragmentActivity { + private static final String TAG = "ImageGridActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (BuildConfig.DEBUG) { + Utils.enableStrictMode(); + } + super.onCreate(savedInstanceState); + + if (getSupportFragmentManager().findFragmentByTag(TAG) == null) { + final FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + ft.add(android.R.id.content, new ImageGridFragment(), TAG); + ft.commit(); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridFragment.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridFragment.java new file mode 100644 index 000000000..4eb1f1b35 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/ImageGridFragment.java @@ -0,0 +1,336 @@ +/* + * 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.example.android.displayingbitmaps.ui; + +import android.annotation.TargetApi; +import android.app.ActivityOptions; +import android.content.Context; +import android.content.Intent; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.Toast; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; +import com.example.android.displayingbitmaps.R; +import com.example.android.displayingbitmaps.provider.Images; +import com.example.android.displayingbitmaps.util.ImageCache; +import com.example.android.displayingbitmaps.util.ImageFetcher; +import com.example.android.displayingbitmaps.util.Utils; + +/** + * The main fragment that powers the ImageGridActivity screen. Fairly straight forward GridView + * implementation with the key addition being the ImageWorker class w/ImageCache to load children + * asynchronously, keeping the UI nice and smooth and caching thumbnails for quick retrieval. The + * cache is retained over configuration changes like orientation change so the images are populated + * quickly if, for example, the user rotates the device. + */ +public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { + private static final String TAG = "ImageGridFragment"; + private static final String IMAGE_CACHE_DIR = "thumbs"; + + private int mImageThumbSize; + private int mImageThumbSpacing; + private ImageAdapter mAdapter; + private ImageFetcher mImageFetcher; + + /** + * Empty constructor as per the Fragment documentation + */ + public ImageGridFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + + mImageThumbSize = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_size); + mImageThumbSpacing = getResources().getDimensionPixelSize(R.dimen.image_thumbnail_spacing); + + mAdapter = new ImageAdapter(getActivity()); + + ImageCache.ImageCacheParams cacheParams = + new ImageCache.ImageCacheParams(getActivity(), IMAGE_CACHE_DIR); + + cacheParams.setMemCacheSizePercent(0.25f); // Set memory cache to 25% of app memory + + // The ImageFetcher takes care of loading images into our ImageView children asynchronously + mImageFetcher = new ImageFetcher(getActivity(), mImageThumbSize); + mImageFetcher.setLoadingImage(R.drawable.empty_photo); + mImageFetcher.addImageCache(getActivity().getSupportFragmentManager(), cacheParams); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + final View v = inflater.inflate(R.layout.image_grid_fragment, container, false); + final GridView mGridView = (GridView) v.findViewById(R.id.gridView); + mGridView.setAdapter(mAdapter); + mGridView.setOnItemClickListener(this); + mGridView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView absListView, int scrollState) { + // Pause fetcher to ensure smoother scrolling when flinging + if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) { + // Before Honeycomb pause image loading on scroll to help with performance + if (!Utils.hasHoneycomb()) { + mImageFetcher.setPauseWork(true); + } + } else { + mImageFetcher.setPauseWork(false); + } + } + + @Override + public void onScroll(AbsListView absListView, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + } + }); + + // This listener is used to get the final width of the GridView and then calculate the + // number of columns and the width of each column. The width of each column is variable + // as the GridView has stretchMode=columnWidth. The column width is used to set the height + // of each view so we get nice square thumbnails. + mGridView.getViewTreeObserver().addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @TargetApi(VERSION_CODES.JELLY_BEAN) + @Override + public void onGlobalLayout() { + if (mAdapter.getNumColumns() == 0) { + final int numColumns = (int) Math.floor( + mGridView.getWidth() / (mImageThumbSize + mImageThumbSpacing)); + if (numColumns > 0) { + final int columnWidth = + (mGridView.getWidth() / numColumns) - mImageThumbSpacing; + mAdapter.setNumColumns(numColumns); + mAdapter.setItemHeight(columnWidth); + if (BuildConfig.DEBUG) { + Log.d(TAG, "onCreateView - numColumns set to " + numColumns); + } + if (Utils.hasJellyBean()) { + mGridView.getViewTreeObserver() + .removeOnGlobalLayoutListener(this); + } else { + mGridView.getViewTreeObserver() + .removeGlobalOnLayoutListener(this); + } + } + } + } + }); + + return v; + } + + @Override + public void onResume() { + super.onResume(); + mImageFetcher.setExitTasksEarly(false); + mAdapter.notifyDataSetChanged(); + } + + @Override + public void onPause() { + super.onPause(); + mImageFetcher.setPauseWork(false); + mImageFetcher.setExitTasksEarly(true); + mImageFetcher.flushCache(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mImageFetcher.closeCache(); + } + + @TargetApi(VERSION_CODES.JELLY_BEAN) + @Override + public void onItemClick(AdapterView<?> parent, View v, int position, long id) { + final Intent i = new Intent(getActivity(), ImageDetailActivity.class); + i.putExtra(ImageDetailActivity.EXTRA_IMAGE, (int) id); + if (Utils.hasJellyBean()) { + // makeThumbnailScaleUpAnimation() looks kind of ugly here as the loading spinner may + // show plus the thumbnail image in GridView is cropped. so using + // makeScaleUpAnimation() instead. + ActivityOptions options = + ActivityOptions.makeScaleUpAnimation(v, 0, 0, v.getWidth(), v.getHeight()); + getActivity().startActivity(i, options.toBundle()); + } else { + startActivity(i); + } + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.main_menu, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.clear_cache: + mImageFetcher.clearCache(); + Toast.makeText(getActivity(), R.string.clear_cache_complete_toast, + Toast.LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * The main adapter that backs the GridView. This is fairly standard except the number of + * columns in the GridView is used to create a fake top row of empty views as we use a + * transparent ActionBar and don't want the real top row of images to start off covered by it. + */ + private class ImageAdapter extends BaseAdapter { + + private final Context mContext; + private int mItemHeight = 0; + private int mNumColumns = 0; + private int mActionBarHeight = 0; + private GridView.LayoutParams mImageViewLayoutParams; + + public ImageAdapter(Context context) { + super(); + mContext = context; + mImageViewLayoutParams = new GridView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + // Calculate ActionBar height + TypedValue tv = new TypedValue(); + if (context.getTheme().resolveAttribute( + android.R.attr.actionBarSize, tv, true)) { + mActionBarHeight = TypedValue.complexToDimensionPixelSize( + tv.data, context.getResources().getDisplayMetrics()); + } + } + + @Override + public int getCount() { + // If columns have yet to be determined, return no items + if (getNumColumns() == 0) { + return 0; + } + + // Size + number of columns for top empty row + return Images.imageThumbUrls.length + mNumColumns; + } + + @Override + public Object getItem(int position) { + return position < mNumColumns ? + null : Images.imageThumbUrls[position - mNumColumns]; + } + + @Override + public long getItemId(int position) { + return position < mNumColumns ? 0 : position - mNumColumns; + } + + @Override + public int getViewTypeCount() { + // Two types of views, the normal ImageView and the top row of empty views + return 2; + } + + @Override + public int getItemViewType(int position) { + return (position < mNumColumns) ? 1 : 0; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public View getView(int position, View convertView, ViewGroup container) { + //BEGIN_INCLUDE(load_gridview_item) + // First check if this is the top row + if (position < mNumColumns) { + if (convertView == null) { + convertView = new View(mContext); + } + // Set empty view with height of ActionBar + convertView.setLayoutParams(new AbsListView.LayoutParams( + LayoutParams.MATCH_PARENT, mActionBarHeight)); + return convertView; + } + + // Now handle the main ImageView thumbnails + ImageView imageView; + if (convertView == null) { // if it's not recycled, instantiate and initialize + imageView = new RecyclingImageView(mContext); + imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); + imageView.setLayoutParams(mImageViewLayoutParams); + } else { // Otherwise re-use the converted view + imageView = (ImageView) convertView; + } + + // Check the height matches our calculated column width + if (imageView.getLayoutParams().height != mItemHeight) { + imageView.setLayoutParams(mImageViewLayoutParams); + } + + // Finally load the image asynchronously into the ImageView, this also takes care of + // setting a placeholder image while the background thread runs + mImageFetcher.loadImage(Images.imageThumbUrls[position - mNumColumns], imageView); + return imageView; + //END_INCLUDE(load_gridview_item) + } + + /** + * Sets the item height. Useful for when we know the column width so the height can be set + * to match. + * + * @param height + */ + public void setItemHeight(int height) { + if (height == mItemHeight) { + return; + } + mItemHeight = height; + mImageViewLayoutParams = + new GridView.LayoutParams(LayoutParams.MATCH_PARENT, mItemHeight); + mImageFetcher.setImageSize(height); + notifyDataSetChanged(); + } + + public void setNumColumns(int numColumns) { + mNumColumns = numColumns; + } + + public int getNumColumns() { + return mNumColumns; + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/RecyclingImageView.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/RecyclingImageView.java new file mode 100644 index 000000000..1db134c31 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/ui/RecyclingImageView.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.displayingbitmaps.ui; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.example.android.displayingbitmaps.util.RecyclingBitmapDrawable; + +/** + * Sub-class of ImageView which automatically notifies the drawable when it is + * being displayed. + */ +public class RecyclingImageView extends ImageView { + + public RecyclingImageView(Context context) { + super(context); + } + + public RecyclingImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + /** + * @see android.widget.ImageView#onDetachedFromWindow() + */ + @Override + protected void onDetachedFromWindow() { + // This has been detached from Window, so clear the drawable + setImageDrawable(null); + + super.onDetachedFromWindow(); + } + + /** + * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable) + */ + @Override + public void setImageDrawable(Drawable drawable) { + // Keep hold of previous Drawable + final Drawable previousDrawable = getDrawable(); + + // Call super to set new Drawable + super.setImageDrawable(drawable); + + // Notify new Drawable that it is being displayed + notifyDrawable(drawable, true); + + // Notify old Drawable so it is no longer being displayed + notifyDrawable(previousDrawable, false); + } + + /** + * Notifies the drawable that it's displayed state has changed. + * + * @param drawable + * @param isDisplayed + */ + private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) { + if (drawable instanceof RecyclingBitmapDrawable) { + // The drawable is a CountingBitmapDrawable, so notify it + ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed); + } else if (drawable instanceof LayerDrawable) { + // The drawable is a LayerDrawable, so recurse on each layer + LayerDrawable layerDrawable = (LayerDrawable) drawable; + for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { + notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); + } + } + } + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/AsyncTask.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/AsyncTask.java new file mode 100644 index 000000000..dffade48e --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/AsyncTask.java @@ -0,0 +1,693 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.displayingbitmaps.util; + +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Message; +import android.os.Process; + +import java.util.ArrayDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * ************************************* + * Copied from JB release framework: + * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java + * + * so that threading behavior on all OS versions is the same and we can tweak behavior by using + * executeOnExecutor() if needed. + * + * There are 3 changes in this copy of AsyncTask: + * -pre-HC a single thread executor is used for serial operation + * (Executors.newSingleThreadExecutor) and is the default + * -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy + * -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added + * ************************************* + * + * <p>AsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.</p> + * + * <p>AsyncTask is designed to be a helper class around {@link Thread} and {@link android.os.Handler} + * and does not constitute a generic threading framework. AsyncTasks should ideally be + * used for short operations (a few seconds at the most.) If you need to keep threads + * running for long periods of time, it is highly recommended you use the various APIs + * provided by the <code>java.util.concurrent</code> pacakge such as {@link java.util.concurrent.Executor}, + * {@link java.util.concurrent.ThreadPoolExecutor} and {@link java.util.concurrent.FutureTask}.</p> + * + * <p>An asynchronous task is defined by a computation that runs on a background thread and + * whose result is published on the UI thread. An asynchronous task is defined by 3 generic + * types, called <code>Params</code>, <code>Progress</code> and <code>Result</code>, + * and 4 steps, called <code>onPreExecute</code>, <code>doInBackground</code>, + * <code>onProgressUpdate</code> and <code>onPostExecute</code>.</p> + * + * <div class="special reference"> + * <h3>Developer Guides</h3> + * <p>For more information about using tasks and threads, read the + * <a href="{@docRoot}guide/topics/fundamentals/processes-and-threads.html">Processes and + * Threads</a> developer guide.</p> + * </div> + * + * <h2>Usage</h2> + * <p>AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)</p> + * + * <p>Here is an example of subclassing:</p> + * <pre class="prettyprint"> + * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> { + * protected Long doInBackground(URL... urls) { + * int count = urls.length; + * long totalSize = 0; + * for (int i = 0; i < count; i++) { + * totalSize += Downloader.downloadFile(urls[i]); + * publishProgress((int) ((i / (float) count) * 100)); + * // Escape early if cancel() is called + * if (isCancelled()) break; + * } + * return totalSize; + * } + * + * protected void onProgressUpdate(Integer... progress) { + * setProgressPercent(progress[0]); + * } + * + * protected void onPostExecute(Long result) { + * showDialog("Downloaded " + result + " bytes"); + * } + * } + * </pre> + * + * <p>Once created, a task is executed very simply:</p> + * <pre class="prettyprint"> + * new DownloadFilesTask().execute(url1, url2, url3); + * </pre> + * + * <h2>AsyncTask's generic types</h2> + * <p>The three types used by an asynchronous task are the following:</p> + * <ol> + * <li><code>Params</code>, the type of the parameters sent to the task upon + * execution.</li> + * <li><code>Progress</code>, the type of the progress units published during + * the background computation.</li> + * <li><code>Result</code>, the type of the result of the background + * computation.</li> + * </ol> + * <p>Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:</p> + * <pre> + * private class MyTask extends AsyncTask<Void, Void, Void> { ... } + * </pre> + * + * <h2>The 4 steps</h2> + * <p>When an asynchronous task is executed, the task goes through 4 steps:</p> + * <ol> + * <li>{@link #onPreExecute()}, invoked on the UI thread immediately after the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.</li> + * <li>{@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.</li> + * <li>{@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.</li> + * <li>{@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.</li> + * </ol> + * + * <h2>Cancelling a task</h2> + * <p>A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking + * this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled(Object)}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)</p> + * + * <h2>Threading rules</h2> + * <p>There are a few threading rules that must be followed for this class to + * work properly:</p> + * <ul> + * <li>The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.</li> + * <li>The task instance must be created on the UI thread.</li> + * <li>{@link #execute} must be invoked on the UI thread.</li> + * <li>Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.</li> + * <li>The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)</li> + * </ul> + * + * <h2>Memory observability</h2> + * <p>AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.</p> + * <ul> + * <li>Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + * <li>Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + * </ul> + * + * <h2>Order of execution</h2> + * <p>When first introduced, AsyncTasks were executed serially on a single background + * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting with + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single + * thread to avoid common application errors caused by parallel execution.</p> + * <p>If you truly want parallel execution, you can invoke + * {@link #executeOnExecutor(java.util.concurrent.Executor, Object[])} with + * {@link #THREAD_POOL_EXECUTOR}.</p> + */ +public abstract class AsyncTask<Params, Progress, Result> { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue<Runnable> sPoolWorkQueue = + new LinkedBlockingQueue<Runnable>(10); + + /** + * An {@link java.util.concurrent.Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory, + new ThreadPoolExecutor.DiscardOldestPolicy()); + + /** + * An {@link java.util.concurrent.Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() : + Executors.newSingleThreadExecutor(sThreadFactory); + + public static final Executor DUAL_THREAD_EXECUTOR = + Executors.newFixedThreadPool(2, sThreadFactory); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static final InternalHandler sHandler = new InternalHandler(); + + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private final WorkerRunnable<Params, Result> mWorker; + private final FutureTask<Result> mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + @TargetApi(11) + private static class SerialExecutor implements Executor { + final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable<Params, Result>() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask<Result>(mWorker) { + @Override + protected void done() { + try { + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + @SuppressWarnings("unchecked") + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult<Result>(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + * <p>Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.</p> + * + * <p>This method won't be invoked if the task was cancelled.</p> + * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.</p> + * + * <p>The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * <code>super.onCancelled(result)</code>.</p> + * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + * <p>Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.</p> + * + * <p>Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.</p> + * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns <tt>true</tt> if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return <tt>true</tt> if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + * <p>Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when <tt>cancel</tt> is called, + * this task should never run. If the task has already started, + * then the <tt>mayInterruptIfRunning</tt> parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.</p> + * + * <p>Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.</p> + * + * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return <tt>false</tt> if the task could not be cancelled, + * typically because it has already completed normally; + * <tt>true</tt> otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws java.util.concurrent.CancellationException If the computation was cancelled. + * @throws java.util.concurrent.ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws java.util.concurrent.CancellationException If the computation was cancelled. + * @throws java.util.concurrent.ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws java.util.concurrent.TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + * <p>Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being + * executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings + * on its use. + * + * <p>This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + * @see #execute(Runnable) + */ + public final AsyncTask<Params, Progress, Result> execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + * <p>This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link java.util.concurrent.Executor} for custom + * behavior. + * + * <p><em>Warning:</em> Allowing multiple tasks to run in parallel from + * a thread pool is generally <em>not</em> what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. Such changes are best + * executed in serial; to guarantee such work is serialized regardless of + * platform version you can use this function with {@link #SERIAL_EXECUTOR}. + * + * <p>This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #execute(Object[]) + */ + public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. See {@link #execute(Object[])} for more + * information on the order of execution. + * + * @see #execute(Object[]) + * @see #executeOnExecutor(java.util.concurrent.Executor, Object[]) + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult<Progress>(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult<Data> { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +}
\ No newline at end of file diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.java new file mode 100644 index 000000000..4c14345ae --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/DiskLruCache.java @@ -0,0 +1,953 @@ +/* + * 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. + */ + +package com.example.android.displayingbitmaps.util; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + ****************************************************************************** + * Taken from the JB source code, can be found in: + * libcore/luni/src/main/java/libcore/io/DiskLruCache.java + * or direct link: + * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java + ****************************************************************************** + * + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Values are byte + * sequences, accessible as streams or files. Each value must be between {@code + * 0} and {@code Integer.MAX_VALUE} bytes in length. + * + * <p>The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + * <p>This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + * <p>Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + * <ul> + * <li>When an entry is being <strong>created</strong> it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + * <li>When an entry is being <strong>edited</strong>, it is not necessary + * to supply data for every value; values default to their previous + * value. + * </ul> + * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + * <p>Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + * <p>This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final int IO_BUFFER_SIZE = 8 * 1024; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap<String, Entry> lruEntries + = new LinkedHashMap<String, Entry>(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /* From java.util.Arrays */ + @SuppressWarnings("unchecked") + private static <T> T[] copyOfRange(T[] original, int start, int end) { + final int originalLength = original.length; // For exception priority compatibility. + if (start > end) { + throw new IllegalArgumentException(); + } + if (start < 0 || start > originalLength) { + throw new ArrayIndexOutOfBoundsException(); + } + final int resultLength = end - start; + final int copyLength = Math.min(resultLength, originalLength - start); + final T[] result = (T[]) Array + .newInstance(original.getClass().getComponentType(), resultLength); + System.arraycopy(original, start, result, 0, copyLength); + return result; + } + + /** + * Returns the remainder of 'reader' as a string, closing it when done. + */ + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); + } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Recursively delete everything in {@code dir}. + */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); + private final Callable<Void> cleanupCallable = new Callable<Void>() { + @Override public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws java.io.IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), + IO_BUFFER_SIZE); + return cache; + } catch (IOException journalIsCorrupt) { +// System.logW("DiskLruCache " + directory + " is corrupt: " +// + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE); + try { + String magic = readAsciiLine(in); + String version = readAsciiLine(in); + String appVersionString = readAsciiLine(in); + String valueCountString = readAsciiLine(in); + String blank = readAsciiLine(in); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); + } + + while (true) { + try { + readJournalLine(readAsciiLine(in)); + } catch (EOFException endOfJournal) { + break; + } + } + } finally { + closeQuietly(in); + } + } + + private void readJournalLine(String line) throws IOException { + String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); + } + + String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); + } + + private static void deleteIfExists(File file) throws IOException { +// try { +// Libcore.os.remove(file.getPath()); +// } catch (ErrnoException errnoException) { +// if (errnoException.errno != OsConstants.ENOENT) { +// throw errnoException.rethrowAsIOException(); +// } +// } + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList<Entry>(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { +// Map.Entry<String, Entry> toEvict = lruEntries.eldest(); + final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return readFully(new InputStreamReader(in, UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override public void close() { + for (InputStream in : ins) { + closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageCache.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageCache.java new file mode 100644 index 000000000..41da56c0b --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageCache.java @@ -0,0 +1,738 @@ +/* + * 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.example.android.displayingbitmaps.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Environment; +import android.os.StatFs; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.util.LruCache; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * This class handles disk and memory caching of bitmaps in conjunction with the + * {@link ImageWorker} class and its subclasses. Use + * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to get an instance of this + * class, although usually a cache should be added directly to an {@link ImageWorker} by calling + * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCacheParams)}. + */ +public class ImageCache { + private static final String TAG = "ImageCache"; + + // Default memory cache size in kilobytes + private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB + + // Default disk cache size in bytes + private static final int DEFAULT_DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB + + // Compression settings when writing images to disk cache + private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; + private static final int DEFAULT_COMPRESS_QUALITY = 70; + private static final int DISK_CACHE_INDEX = 0; + + // Constants to easily toggle various caches + private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; + private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; + private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false; + + private DiskLruCache mDiskLruCache; + private LruCache<String, BitmapDrawable> mMemoryCache; + private ImageCacheParams mCacheParams; + private final Object mDiskCacheLock = new Object(); + private boolean mDiskCacheStarting = true; + + private Set<SoftReference<Bitmap>> mReusableBitmaps; + + /** + * Create a new ImageCache object using the specified parameters. This should not be + * called directly by other classes, instead use + * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} to fetch an ImageCache + * instance. + * + * @param cacheParams The cache parameters to use to initialize the cache + */ + private ImageCache(ImageCacheParams cacheParams) { + init(cacheParams); + } + + /** + * Return an {@link ImageCache} instance. A {@link RetainFragment} is used to retain the + * ImageCache object across configuration changes such as a change in device orientation. + * + * @param fragmentManager The fragment manager to use when dealing with the retained fragment. + * @param cacheParams The cache parameters to use if the ImageCache needs instantiation. + * @return An existing retained ImageCache object or a new one if one did not exist + */ + public static ImageCache getInstance( + FragmentManager fragmentManager, ImageCacheParams cacheParams) { + + // Search for, or create an instance of the non-UI RetainFragment + final RetainFragment mRetainFragment = findOrCreateRetainFragment(fragmentManager); + + // See if we already have an ImageCache stored in RetainFragment + ImageCache imageCache = (ImageCache) mRetainFragment.getObject(); + + // No existing ImageCache, create one and store it in RetainFragment + if (imageCache == null) { + imageCache = new ImageCache(cacheParams); + mRetainFragment.setObject(imageCache); + } + + return imageCache; + } + + /** + * Initialize the cache, providing all parameters. + * + * @param cacheParams The cache parameters to initialize the cache + */ + private void init(ImageCacheParams cacheParams) { + mCacheParams = cacheParams; + + //BEGIN_INCLUDE(init_memory_cache) + // Set up memory cache + if (mCacheParams.memoryCacheEnabled) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); + } + + // If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be + // populated into the inBitmap field of BitmapFactory.Options. Note that the set is + // of SoftReferences which will actually not be very effective due to the garbage + // collector being aggressive clearing Soft/WeakReferences. A better approach + // would be to use a strongly references bitmaps, however this would require some + // balancing of memory usage between this set and the bitmap LruCache. It would also + // require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean + // the size would need to be precise, from KitKat onward the size would just need to + // be the upper bound (due to changes in how inBitmap can re-use bitmaps). + if (Utils.hasHoneycomb()) { + mReusableBitmaps = + Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>()); + } + + mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) { + + /** + * Notify the removed entry that is no longer being cached + */ + @Override + protected void entryRemoved(boolean evicted, String key, + BitmapDrawable oldValue, BitmapDrawable newValue) { + if (RecyclingBitmapDrawable.class.isInstance(oldValue)) { + // The removed entry is a recycling drawable, so notify it + // that it has been removed from the memory cache + ((RecyclingBitmapDrawable) oldValue).setIsCached(false); + } else { + // The removed entry is a standard BitmapDrawable + + if (Utils.hasHoneycomb()) { + // We're running on Honeycomb or later, so add the bitmap + // to a SoftReference set for possible use with inBitmap later + mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap())); + } + } + } + + /** + * Measure item size in kilobytes rather than units which is more practical + * for a bitmap cache + */ + @Override + protected int sizeOf(String key, BitmapDrawable value) { + final int bitmapSize = getBitmapSize(value) / 1024; + return bitmapSize == 0 ? 1 : bitmapSize; + } + }; + } + //END_INCLUDE(init_memory_cache) + + // By default the disk cache is not initialized here as it should be initialized + // on a separate thread due to disk access. + if (cacheParams.initDiskCacheOnCreate) { + // Set up disk cache + initDiskCache(); + } + } + + /** + * Initializes the disk cache. Note that this includes disk access so this should not be + * executed on the main/UI thread. By default an ImageCache does not initialize the disk + * cache when it is created, instead you should call initDiskCache() to initialize it on a + * background thread. + */ + public void initDiskCache() { + // Set up disk cache + synchronized (mDiskCacheLock) { + if (mDiskLruCache == null || mDiskLruCache.isClosed()) { + File diskCacheDir = mCacheParams.diskCacheDir; + if (mCacheParams.diskCacheEnabled && diskCacheDir != null) { + if (!diskCacheDir.exists()) { + diskCacheDir.mkdirs(); + } + if (getUsableSpace(diskCacheDir) > mCacheParams.diskCacheSize) { + try { + mDiskLruCache = DiskLruCache.open( + diskCacheDir, 1, 1, mCacheParams.diskCacheSize); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache initialized"); + } + } catch (final IOException e) { + mCacheParams.diskCacheDir = null; + Log.e(TAG, "initDiskCache - " + e); + } + } + } + } + mDiskCacheStarting = false; + mDiskCacheLock.notifyAll(); + } + } + + /** + * Adds a bitmap to both memory and disk cache. + * @param data Unique identifier for the bitmap to store + * @param value The bitmap drawable to store + */ + public void addBitmapToCache(String data, BitmapDrawable value) { + //BEGIN_INCLUDE(add_bitmap_to_cache) + if (data == null || value == null) { + return; + } + + // Add to memory cache + if (mMemoryCache != null) { + if (RecyclingBitmapDrawable.class.isInstance(value)) { + // The removed entry is a recycling drawable, so notify it + // that it has been added into the memory cache + ((RecyclingBitmapDrawable) value).setIsCached(true); + } + mMemoryCache.put(data, value); + } + + synchronized (mDiskCacheLock) { + // Add to disk cache + if (mDiskLruCache != null) { + final String key = hashKeyForDisk(data); + OutputStream out = null; + try { + DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); + if (snapshot == null) { + final DiskLruCache.Editor editor = mDiskLruCache.edit(key); + if (editor != null) { + out = editor.newOutputStream(DISK_CACHE_INDEX); + value.getBitmap().compress( + mCacheParams.compressFormat, mCacheParams.compressQuality, out); + editor.commit(); + out.close(); + } + } else { + snapshot.getInputStream(DISK_CACHE_INDEX).close(); + } + } catch (final IOException e) { + Log.e(TAG, "addBitmapToCache - " + e); + } catch (Exception e) { + Log.e(TAG, "addBitmapToCache - " + e); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException e) {} + } + } + } + //END_INCLUDE(add_bitmap_to_cache) + } + + /** + * Get from memory cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap drawable if found in cache, null otherwise + */ + public BitmapDrawable getBitmapFromMemCache(String data) { + //BEGIN_INCLUDE(get_bitmap_from_mem_cache) + BitmapDrawable memValue = null; + + if (mMemoryCache != null) { + memValue = mMemoryCache.get(data); + } + + if (BuildConfig.DEBUG && memValue != null) { + Log.d(TAG, "Memory cache hit"); + } + + return memValue; + //END_INCLUDE(get_bitmap_from_mem_cache) + } + + /** + * Get from disk cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap if found in cache, null otherwise + */ + public Bitmap getBitmapFromDiskCache(String data) { + //BEGIN_INCLUDE(get_bitmap_from_disk_cache) + final String key = hashKeyForDisk(data); + Bitmap bitmap = null; + + synchronized (mDiskCacheLock) { + while (mDiskCacheStarting) { + try { + mDiskCacheLock.wait(); + } catch (InterruptedException e) {} + } + if (mDiskLruCache != null) { + InputStream inputStream = null; + try { + final DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); + if (snapshot != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache hit"); + } + inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); + if (inputStream != null) { + FileDescriptor fd = ((FileInputStream) inputStream).getFD(); + + // Decode bitmap, but we don't want to sample so give + // MAX_VALUE as the target dimensions + bitmap = ImageResizer.decodeSampledBitmapFromDescriptor( + fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this); + } + } + } catch (final IOException e) { + Log.e(TAG, "getBitmapFromDiskCache - " + e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) {} + } + } + return bitmap; + } + //END_INCLUDE(get_bitmap_from_disk_cache) + } + + /** + * @param options - BitmapFactory.Options with out* options populated + * @return Bitmap that case be used for inBitmap + */ + protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { + //BEGIN_INCLUDE(get_bitmap_from_reusable_set) + Bitmap bitmap = null; + + if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { + synchronized (mReusableBitmaps) { + final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator(); + Bitmap item; + + while (iterator.hasNext()) { + item = iterator.next().get(); + + if (null != item && item.isMutable()) { + // Check to see it the item can be used for inBitmap + if (canUseForInBitmap(item, options)) { + bitmap = item; + + // Remove from reusable set so it can't be used again + iterator.remove(); + break; + } + } else { + // Remove from the set if the reference has been cleared. + iterator.remove(); + } + } + } + } + + return bitmap; + //END_INCLUDE(get_bitmap_from_reusable_set) + } + + /** + * Clears both the memory and disk cache associated with this ImageCache object. Note that + * this includes disk access so this should not be executed on the main/UI thread. + */ + public void clearCache() { + if (mMemoryCache != null) { + mMemoryCache.evictAll(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Memory cache cleared"); + } + } + + synchronized (mDiskCacheLock) { + mDiskCacheStarting = true; + if (mDiskLruCache != null && !mDiskLruCache.isClosed()) { + try { + mDiskLruCache.delete(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache cleared"); + } + } catch (IOException e) { + Log.e(TAG, "clearCache - " + e); + } + mDiskLruCache = null; + initDiskCache(); + } + } + } + + /** + * Flushes the disk cache associated with this ImageCache object. Note that this includes + * disk access so this should not be executed on the main/UI thread. + */ + public void flush() { + synchronized (mDiskCacheLock) { + if (mDiskLruCache != null) { + try { + mDiskLruCache.flush(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache flushed"); + } + } catch (IOException e) { + Log.e(TAG, "flush - " + e); + } + } + } + } + + /** + * Closes the disk cache associated with this ImageCache object. Note that this includes + * disk access so this should not be executed on the main/UI thread. + */ + public void close() { + synchronized (mDiskCacheLock) { + if (mDiskLruCache != null) { + try { + if (!mDiskLruCache.isClosed()) { + mDiskLruCache.close(); + mDiskLruCache = null; + if (BuildConfig.DEBUG) { + Log.d(TAG, "Disk cache closed"); + } + } + } catch (IOException e) { + Log.e(TAG, "close - " + e); + } + } + } + } + + /** + * A holder class that contains cache parameters. + */ + public static class ImageCacheParams { + public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; + public int diskCacheSize = DEFAULT_DISK_CACHE_SIZE; + public File diskCacheDir; + public CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT; + public int compressQuality = DEFAULT_COMPRESS_QUALITY; + public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; + public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; + public boolean initDiskCacheOnCreate = DEFAULT_INIT_DISK_CACHE_ON_CREATE; + + /** + * Create a set of image cache parameters that can be provided to + * {@link ImageCache#getInstance(android.support.v4.app.FragmentManager, ImageCacheParams)} or + * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCacheParams)}. + * @param context A context to use. + * @param diskCacheDirectoryName A unique subdirectory name that will be appended to the + * application cache directory. Usually "cache" or "images" + * is sufficient. + */ + public ImageCacheParams(Context context, String diskCacheDirectoryName) { + diskCacheDir = getDiskCacheDir(context, diskCacheDirectoryName); + } + + /** + * Sets the memory cache size based on a percentage of the max available VM memory. + * Eg. setting percent to 0.2 would set the memory cache to one fifth of the available + * memory. Throws {@link IllegalArgumentException} if percent is < 0.01 or > .8. + * memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed + * to construct a LruCache which takes an int in its constructor. + * + * This value should be chosen carefully based on a number of factors + * Refer to the corresponding Android Training class for more discussion: + * http://developer.android.com/training/displaying-bitmaps/ + * + * @param percent Percent of available app memory to use to size memory cache + */ + public void setMemCacheSizePercent(float percent) { + if (percent < 0.01f || percent > 0.8f) { + throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " + + "between 0.01 and 0.8 (inclusive)"); + } + memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024); + } + } + + /** + * @param candidate - Bitmap to check + * @param targetOptions - Options that have the out* value populated + * @return true if <code>candidate</code> can be used for inBitmap re-use with + * <code>targetOptions</code> + */ + @TargetApi(VERSION_CODES.KITKAT) + private static boolean canUseForInBitmap( + Bitmap candidate, BitmapFactory.Options targetOptions) { + //BEGIN_INCLUDE(can_use_for_inbitmap) + if (!Utils.hasKitKat()) { + // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 + return candidate.getWidth() == targetOptions.outWidth + && candidate.getHeight() == targetOptions.outHeight + && targetOptions.inSampleSize == 1; + } + + // From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap + // is smaller than the reusable bitmap candidate allocation byte count. + int width = targetOptions.outWidth / targetOptions.inSampleSize; + int height = targetOptions.outHeight / targetOptions.inSampleSize; + int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); + return byteCount <= candidate.getAllocationByteCount(); + //END_INCLUDE(can_use_for_inbitmap) + } + + /** + * Return the byte usage per pixel of a bitmap based on its configuration. + * @param config The bitmap configuration. + * @return The byte usage per pixel. + */ + private static int getBytesPerPixel(Config config) { + if (config == Config.ARGB_8888) { + return 4; + } else if (config == Config.RGB_565) { + return 2; + } else if (config == Config.ARGB_4444) { + return 2; + } else if (config == Config.ALPHA_8) { + return 1; + } + return 1; + } + + /** + * Get a usable cache directory (external if available, internal otherwise). + * + * @param context The context to use + * @param uniqueName A unique directory name to append to the cache dir + * @return The cache dir + */ + public static File getDiskCacheDir(Context context, String uniqueName) { + // Check if media is mounted or storage is built-in, if so, try and use external cache dir + // otherwise use internal cache dir + final String cachePath = + Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || + !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() : + context.getCacheDir().getPath(); + + return new File(cachePath + File.separator + uniqueName); + } + + /** + * A hashing method that changes a string (like a URL) into a hash suitable for using as a + * disk filename. + */ + public static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + // http://stackoverflow.com/questions/332079 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + /** + * Get the size in bytes of a bitmap in a BitmapDrawable. Note that from Android 4.4 (KitKat) + * onward this returns the allocated memory size of the bitmap which can be larger than the + * actual bitmap data byte count (in the case it was re-used). + * + * @param value + * @return size in bytes + */ + @TargetApi(VERSION_CODES.KITKAT) + public static int getBitmapSize(BitmapDrawable value) { + Bitmap bitmap = value.getBitmap(); + + // From KitKat onward use getAllocationByteCount() as allocated bytes can potentially be + // larger than bitmap byte count. + if (Utils.hasKitKat()) { + return bitmap.getAllocationByteCount(); + } + + if (Utils.hasHoneycombMR1()) { + return bitmap.getByteCount(); + } + + // Pre HC-MR1 + return bitmap.getRowBytes() * bitmap.getHeight(); + } + + /** + * Check if external storage is built-in or removable. + * + * @return True if external storage is removable (like an SD card), false + * otherwise. + */ + @TargetApi(VERSION_CODES.GINGERBREAD) + public static boolean isExternalStorageRemovable() { + if (Utils.hasGingerbread()) { + return Environment.isExternalStorageRemovable(); + } + return true; + } + + /** + * Get the external app cache directory. + * + * @param context The context to use + * @return The external cache dir + */ + @TargetApi(VERSION_CODES.FROYO) + public static File getExternalCacheDir(Context context) { + if (Utils.hasFroyo()) { + return context.getExternalCacheDir(); + } + + // Before Froyo we need to construct the external cache dir ourselves + final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; + return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir); + } + + /** + * Check how much usable space is available at a given path. + * + * @param path The path to check + * @return The space available in bytes + */ + @TargetApi(VERSION_CODES.GINGERBREAD) + public static long getUsableSpace(File path) { + if (Utils.hasGingerbread()) { + return path.getUsableSpace(); + } + final StatFs stats = new StatFs(path.getPath()); + return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); + } + + /** + * Locate an existing instance of this Fragment or if not found, create and + * add it using FragmentManager. + * + * @param fm The FragmentManager manager to use. + * @return The existing instance of the Fragment or the new instance if just + * created. + */ + private static RetainFragment findOrCreateRetainFragment(FragmentManager fm) { + //BEGIN_INCLUDE(find_create_retain_fragment) + // Check to see if we have retained the worker fragment. + RetainFragment mRetainFragment = (RetainFragment) fm.findFragmentByTag(TAG); + + // If not retained (or first time running), we need to create and add it. + if (mRetainFragment == null) { + mRetainFragment = new RetainFragment(); + fm.beginTransaction().add(mRetainFragment, TAG).commitAllowingStateLoss(); + } + + return mRetainFragment; + //END_INCLUDE(find_create_retain_fragment) + } + + /** + * A simple non-UI Fragment that stores a single Object and is retained over configuration + * changes. It will be used to retain the ImageCache object. + */ + public static class RetainFragment extends Fragment { + private Object mObject; + + /** + * Empty constructor as per the Fragment documentation + */ + public RetainFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Make sure this Fragment is retained over a configuration change + setRetainInstance(true); + } + + /** + * Store a single object in this Fragment. + * + * @param object The object to store + */ + public void setObject(Object object) { + mObject = object; + } + + /** + * Get the stored object. + * + * @return The stored object + */ + public Object getObject() { + return mObject; + } + } + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageFetcher.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageFetcher.java new file mode 100644 index 000000000..b9ccb68cf --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageFetcher.java @@ -0,0 +1,311 @@ +/* + * 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.example.android.displayingbitmaps.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.widget.Toast; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; +import com.example.android.displayingbitmaps.R; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * A simple subclass of {@link ImageResizer} that fetches and resizes images fetched from a URL. + */ +public class ImageFetcher extends ImageResizer { + private static final String TAG = "ImageFetcher"; + private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB + private static final String HTTP_CACHE_DIR = "http"; + private static final int IO_BUFFER_SIZE = 8 * 1024; + + private DiskLruCache mHttpDiskCache; + private File mHttpCacheDir; + private boolean mHttpDiskCacheStarting = true; + private final Object mHttpDiskCacheLock = new Object(); + private static final int DISK_CACHE_INDEX = 0; + + /** + * Initialize providing a target image width and height for the processing images. + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageFetcher(Context context, int imageWidth, int imageHeight) { + super(context, imageWidth, imageHeight); + init(context); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageFetcher(Context context, int imageSize) { + super(context, imageSize); + init(context); + } + + private void init(Context context) { + checkConnection(context); + mHttpCacheDir = ImageCache.getDiskCacheDir(context, HTTP_CACHE_DIR); + } + + @Override + protected void initDiskCacheInternal() { + super.initDiskCacheInternal(); + initHttpDiskCache(); + } + + private void initHttpDiskCache() { + if (!mHttpCacheDir.exists()) { + mHttpCacheDir.mkdirs(); + } + synchronized (mHttpDiskCacheLock) { + if (ImageCache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) { + try { + mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache initialized"); + } + } catch (IOException e) { + mHttpDiskCache = null; + } + } + mHttpDiskCacheStarting = false; + mHttpDiskCacheLock.notifyAll(); + } + } + + @Override + protected void clearCacheInternal() { + super.clearCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) { + try { + mHttpDiskCache.delete(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache cleared"); + } + } catch (IOException e) { + Log.e(TAG, "clearCacheInternal - " + e); + } + mHttpDiskCache = null; + mHttpDiskCacheStarting = true; + initHttpDiskCache(); + } + } + } + + @Override + protected void flushCacheInternal() { + super.flushCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + mHttpDiskCache.flush(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache flushed"); + } + } catch (IOException e) { + Log.e(TAG, "flush - " + e); + } + } + } + } + + @Override + protected void closeCacheInternal() { + super.closeCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + if (!mHttpDiskCache.isClosed()) { + mHttpDiskCache.close(); + mHttpDiskCache = null; + if (BuildConfig.DEBUG) { + Log.d(TAG, "HTTP cache closed"); + } + } + } catch (IOException e) { + Log.e(TAG, "closeCacheInternal - " + e); + } + } + } + } + + /** + * Simple network connection check. + * + * @param context + */ + private void checkConnection(Context context) { + final ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo == null || !networkInfo.isConnectedOrConnecting()) { + Toast.makeText(context, R.string.no_network_connection_toast, Toast.LENGTH_LONG).show(); + Log.e(TAG, "checkConnection - no connection found"); + } + } + + /** + * The main process method, which will be called by the ImageWorker in the AsyncTask background + * thread. + * + * @param data The data to load the bitmap, in this case, a regular http URL + * @return The downloaded and resized bitmap + */ + private Bitmap processBitmap(String data) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + data); + } + + final String key = ImageCache.hashKeyForDisk(data); + FileDescriptor fileDescriptor = null; + FileInputStream fileInputStream = null; + DiskLruCache.Snapshot snapshot; + synchronized (mHttpDiskCacheLock) { + // Wait for disk cache to initialize + while (mHttpDiskCacheStarting) { + try { + mHttpDiskCacheLock.wait(); + } catch (InterruptedException e) {} + } + + if (mHttpDiskCache != null) { + try { + snapshot = mHttpDiskCache.get(key); + if (snapshot == null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap, not found in http cache, downloading..."); + } + DiskLruCache.Editor editor = mHttpDiskCache.edit(key); + if (editor != null) { + if (downloadUrlToStream(data, + editor.newOutputStream(DISK_CACHE_INDEX))) { + editor.commit(); + } else { + editor.abort(); + } + } + snapshot = mHttpDiskCache.get(key); + } + if (snapshot != null) { + fileInputStream = + (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); + fileDescriptor = fileInputStream.getFD(); + } + } catch (IOException e) { + Log.e(TAG, "processBitmap - " + e); + } catch (IllegalStateException e) { + Log.e(TAG, "processBitmap - " + e); + } finally { + if (fileDescriptor == null && fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) {} + } + } + } + } + + Bitmap bitmap = null; + if (fileDescriptor != null) { + bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, + mImageHeight, getImageCache()); + } + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) {} + } + return bitmap; + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(String.valueOf(data)); + } + + /** + * Download a bitmap from a URL and write the content to an output stream. + * + * @param urlString The URL to fetch + * @return true if successful, false otherwise + */ + public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { + disableConnectionReuseIfNecessary(); + HttpURLConnection urlConnection = null; + BufferedOutputStream out = null; + BufferedInputStream in = null; + + try { + final URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); + out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); + + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return true; + } catch (final IOException e) { + Log.e(TAG, "Error in downloadBitmap - " + e); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (final IOException e) {} + } + return false; + } + + /** + * Workaround for bug pre-Froyo, see here for more info: + * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + */ + public static void disableConnectionReuseIfNecessary() { + // HTTP connection reuse which was buggy pre-froyo + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + System.setProperty("http.keepAlive", "false"); + } + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageResizer.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageResizer.java new file mode 100644 index 000000000..be9109682 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageResizer.java @@ -0,0 +1,270 @@ +/* + * 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.example.android.displayingbitmaps.util; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; + +import java.io.FileDescriptor; + +/** + * A simple subclass of {@link ImageWorker} that resizes images from resources given a target width + * and height. Useful for when the input images might be too large to simply load directly into + * memory. + */ +public class ImageResizer extends ImageWorker { + private static final String TAG = "ImageResizer"; + protected int mImageWidth; + protected int mImageHeight; + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageWidth + * @param imageHeight + */ + public ImageResizer(Context context, int imageWidth, int imageHeight) { + super(context); + setImageSize(imageWidth, imageHeight); + } + + /** + * Initialize providing a single target image size (used for both width and height); + * + * @param context + * @param imageSize + */ + public ImageResizer(Context context, int imageSize) { + super(context); + setImageSize(imageSize); + } + + /** + * Set the target image width and height. + * + * @param width + * @param height + */ + public void setImageSize(int width, int height) { + mImageWidth = width; + mImageHeight = height; + } + + /** + * Set the target image size (width and height will be the same). + * + * @param size + */ + public void setImageSize(int size) { + setImageSize(size, size); + } + + /** + * The main processing method. This happens in a background task. In this case we are just + * sampling down the bitmap and returning it from a resource. + * + * @param resId + * @return + */ + private Bitmap processBitmap(int resId) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "processBitmap - " + resId); + } + return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, + mImageHeight, getImageCache()); + } + + @Override + protected Bitmap processBitmap(Object data) { + return processBitmap(Integer.parseInt(String.valueOf(data))); + } + + /** + * Decode and sample down a bitmap from resources to the requested width and height. + * + * @param res The resources object containing the image data + * @param resId The resource id of the image data + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, + int reqWidth, int reqHeight, ImageCache cache) { + + // BEGIN_INCLUDE (read_bitmap_dimensions) + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + // END_INCLUDE (read_bitmap_dimensions) + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + + /** + * Decode and sample down a bitmap from a file to the requested width and height. + * + * @param filename The full path of the file to decode + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromFile(String filename, + int reqWidth, int reqHeight, ImageCache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(filename, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + return BitmapFactory.decodeFile(filename, options); + } + + /** + * Decode and sample down a bitmap from a file input stream to the requested width and height. + * + * @param fileDescriptor The file descriptor to read from + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The ImageCache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromDescriptor( + FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) { + //BEGIN_INCLUDE(add_bitmap_options) + // inBitmap only works with mutable bitmaps so force the decoder to + // return mutable bitmaps. + options.inMutable = true; + + if (cache != null) { + // Try and find a bitmap to use for inBitmap + Bitmap inBitmap = cache.getBitmapFromReusableSet(options); + + if (inBitmap != null) { + options.inBitmap = inBitmap; + } + } + //END_INCLUDE(add_bitmap_options) + } + + /** + * Calculate an inSampleSize for use in a {@link android.graphics.BitmapFactory.Options} object when decoding + * bitmaps using the decode* methods from {@link android.graphics.BitmapFactory}. This implementation calculates + * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap + * having a width and height equal to or larger than the requested width and height. + * + * @param options An options object with out* params already populated (run through a decode* + * method with inJustDecodeBounds==true + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return The value to be used for inSampleSize + */ + public static int calculateInSampleSize(BitmapFactory.Options options, + int reqWidth, int reqHeight) { + // BEGIN_INCLUDE (calculate_sample_size) + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight + && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger inSampleSize). + + long totalPixels = width * height / inSampleSize; + + // Anything more than 2x the requested pixels we'll sample down further + final long totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels > totalReqPixelsCap) { + inSampleSize *= 2; + totalPixels /= 2; + } + } + return inSampleSize; + // END_INCLUDE (calculate_sample_size) + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java new file mode 100644 index 000000000..f44d00d5b --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/ImageWorker.java @@ -0,0 +1,485 @@ +/* + * 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.example.android.displayingbitmaps.util; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.widget.ImageView; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; + +import java.lang.ref.WeakReference; + +/** + * This class wraps up completing some arbitrary long running work when loading a bitmap to an + * ImageView. It handles things like using a memory and disk cache, running the work in a background + * thread and setting a placeholder image. + */ +public abstract class ImageWorker { + private static final String TAG = "ImageWorker"; + private static final int FADE_IN_TIME = 200; + + private ImageCache mImageCache; + private ImageCache.ImageCacheParams mImageCacheParams; + private Bitmap mLoadingBitmap; + private boolean mFadeInBitmap = true; + private boolean mExitTasksEarly = false; + protected boolean mPauseWork = false; + private final Object mPauseWorkLock = new Object(); + + protected Resources mResources; + + private static final int MESSAGE_CLEAR = 0; + private static final int MESSAGE_INIT_DISK_CACHE = 1; + private static final int MESSAGE_FLUSH = 2; + private static final int MESSAGE_CLOSE = 3; + + protected ImageWorker(Context context) { + mResources = context.getResources(); + } + + /** + * Load an image specified by the data parameter into an ImageView (override + * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and + * disk cache will be used if an {@link ImageCache} has been added using + * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the + * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} + * will be created to asynchronously load the bitmap. + * + * @param data The URL of the image to download. + * @param imageView The ImageView to bind the downloaded image to. + */ + public void loadImage(Object data, ImageView imageView) { + if (data == null) { + return; + } + + BitmapDrawable value = null; + + if (mImageCache != null) { + value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); + } + + if (value != null) { + // Bitmap found in memory cache + imageView.setImageDrawable(value); + } else if (cancelPotentialWork(data, imageView)) { + //BEGIN_INCLUDE(execute_background_task) + final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView); + final AsyncDrawable asyncDrawable = + new AsyncDrawable(mResources, mLoadingBitmap, task); + imageView.setImageDrawable(asyncDrawable); + + // NOTE: This uses a custom version of AsyncTask that has been pulled from the + // framework and slightly modified. Refer to the docs at the top of the class + // for more info on what was changed. + task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR); + //END_INCLUDE(execute_background_task) + } + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param bitmap + */ + public void setLoadingImage(Bitmap bitmap) { + mLoadingBitmap = bitmap; + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param resId + */ + public void setLoadingImage(int resId) { + mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); + } + + /** + * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap + * caching. + * @param fragmentManager + * @param cacheParams The cache parameters to use for the image cache. + */ + public void addImageCache(FragmentManager fragmentManager, + ImageCache.ImageCacheParams cacheParams) { + mImageCacheParams = cacheParams; + mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams); + new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); + } + + /** + * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap + * caching. + * @param activity + * @param diskCacheDirectoryName See + * {@link ImageCache.ImageCacheParams#ImageCacheParams(android.content.Context, String)}. + */ + public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) { + mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName); + mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams); + new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); + } + + /** + * If set to true, the image will fade-in once it has been loaded by the background thread. + */ + public void setImageFadeIn(boolean fadeIn) { + mFadeInBitmap = fadeIn; + } + + public void setExitTasksEarly(boolean exitTasksEarly) { + mExitTasksEarly = exitTasksEarly; + setPauseWork(false); + } + + /** + * Subclasses should override this to define any processing or work that must happen to produce + * the final bitmap. This will be executed in a background thread and be long running. For + * example, you could resize a large bitmap here, or pull down an image from the network. + * + * @param data The data to identify which image to process, as provided by + * {@link ImageWorker#loadImage(Object, android.widget.ImageView)} + * @return The processed bitmap + */ + protected abstract Bitmap processBitmap(Object data); + + /** + * @return The {@link ImageCache} object currently being used by this ImageWorker. + */ + protected ImageCache getImageCache() { + return mImageCache; + } + + /** + * Cancels any pending work attached to the provided ImageView. + * @param imageView + */ + public static void cancelWork(ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + if (bitmapWorkerTask != null) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + final Object bitmapData = bitmapWorkerTask.mData; + Log.d(TAG, "cancelWork - cancelled work for " + bitmapData); + } + } + } + + /** + * Returns true if the current work has been canceled or if there was no work in + * progress on this image view. + * Returns false if the work in progress deals with the same data. The work is not + * stopped in that case. + */ + public static boolean cancelPotentialWork(Object data, ImageView imageView) { + //BEGIN_INCLUDE(cancel_potential_work) + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Object bitmapData = bitmapWorkerTask.mData; + if (bitmapData == null || !bitmapData.equals(data)) { + bitmapWorkerTask.cancel(true); + if (BuildConfig.DEBUG) { + Log.d(TAG, "cancelPotentialWork - cancelled work for " + data); + } + } else { + // The same work is already in progress. + return false; + } + } + return true; + //END_INCLUDE(cancel_potential_work) + } + + /** + * @param imageView Any imageView + * @return Retrieve the currently active work task (if any) associated with this imageView. + * null if there is no such task. + */ + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + /** + * The actual AsyncTask that will asynchronously process the image. + */ + private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> { + private Object mData; + private final WeakReference<ImageView> imageViewReference; + + public BitmapWorkerTask(Object data, ImageView imageView) { + mData = data; + imageViewReference = new WeakReference<ImageView>(imageView); + } + + /** + * Background processing. + */ + @Override + protected BitmapDrawable doInBackground(Void... params) { + //BEGIN_INCLUDE(load_bitmap_in_background) + if (BuildConfig.DEBUG) { + Log.d(TAG, "doInBackground - starting work"); + } + + final String dataString = String.valueOf(mData); + Bitmap bitmap = null; + BitmapDrawable drawable = null; + + // Wait here if work is paused and the task is not cancelled + synchronized (mPauseWorkLock) { + while (mPauseWork && !isCancelled()) { + try { + mPauseWorkLock.wait(); + } catch (InterruptedException e) {} + } + } + + // If the image cache is available and this task has not been cancelled by another + // thread and the ImageView that was originally bound to this task is still bound back + // to this task and our "exit early" flag is not set then try and fetch the bitmap from + // the cache + if (mImageCache != null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = mImageCache.getBitmapFromDiskCache(dataString); + } + + // If the bitmap was not found in the cache and this task has not been cancelled by + // another thread and the ImageView that was originally bound to this task is still + // bound back to this task and our "exit early" flag is not set, then call the main + // process method (as implemented by a subclass) + if (bitmap == null && !isCancelled() && getAttachedImageView() != null + && !mExitTasksEarly) { + bitmap = processBitmap(mData); + } + + // If the bitmap was processed and the image cache is available, then add the processed + // bitmap to the cache for future use. Note we don't check if the task was cancelled + // here, if it was, and the thread is still running, we may as well add the processed + // bitmap to our cache as it might be used again in the future + if (bitmap != null) { + if (Utils.hasHoneycomb()) { + // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable + drawable = new BitmapDrawable(mResources, bitmap); + } else { + // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable + // which will recycle automagically + drawable = new RecyclingBitmapDrawable(mResources, bitmap); + } + + if (mImageCache != null) { + mImageCache.addBitmapToCache(dataString, drawable); + } + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "doInBackground - finished work"); + } + + return drawable; + //END_INCLUDE(load_bitmap_in_background) + } + + /** + * Once the image is processed, associates it to the imageView + */ + @Override + protected void onPostExecute(BitmapDrawable value) { + //BEGIN_INCLUDE(complete_background_work) + // if cancel was called on this task or the "exit early" flag is set then we're done + if (isCancelled() || mExitTasksEarly) { + value = null; + } + + final ImageView imageView = getAttachedImageView(); + if (value != null && imageView != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "onPostExecute - setting bitmap"); + } + setImageDrawable(imageView, value); + } + //END_INCLUDE(complete_background_work) + } + + @Override + protected void onCancelled(BitmapDrawable value) { + super.onCancelled(value); + synchronized (mPauseWorkLock) { + mPauseWorkLock.notifyAll(); + } + } + + /** + * Returns the ImageView associated with this task as long as the ImageView's task still + * points to this task as well. Returns null otherwise. + */ + private ImageView getAttachedImageView() { + final ImageView imageView = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (this == bitmapWorkerTask) { + return imageView; + } + + return null; + } + } + + /** + * A custom Drawable that will be attached to the imageView while the work is in progress. + * Contains a reference to the actual worker task, so that it can be stopped if a new binding is + * required, and makes sure that only the last started worker process can bind its result, + * independently of the finish order. + */ + private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = + new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + + /** + * Called when the processing is complete and the final drawable should be + * set on the ImageView. + * + * @param imageView + * @param drawable + */ + private void setImageDrawable(ImageView imageView, Drawable drawable) { + if (mFadeInBitmap) { + // Transition drawable with a transparent drawable and the final drawable + final TransitionDrawable td = + new TransitionDrawable(new Drawable[] { + new ColorDrawable(android.R.color.transparent), + drawable + }); + // Set background to loading bitmap + imageView.setBackgroundDrawable( + new BitmapDrawable(mResources, mLoadingBitmap)); + + imageView.setImageDrawable(td); + td.startTransition(FADE_IN_TIME); + } else { + imageView.setImageDrawable(drawable); + } + } + + /** + * Pause any ongoing background work. This can be used as a temporary + * measure to improve performance. For example background work could + * be paused when a ListView or GridView is being scrolled using a + * {@link android.widget.AbsListView.OnScrollListener} to keep + * scrolling smooth. + * <p> + * If work is paused, be sure setPauseWork(false) is called again + * before your fragment or activity is destroyed (for example during + * {@link android.app.Activity#onPause()}), or there is a risk the + * background thread will never finish. + */ + public void setPauseWork(boolean pauseWork) { + synchronized (mPauseWorkLock) { + mPauseWork = pauseWork; + if (!mPauseWork) { + mPauseWorkLock.notifyAll(); + } + } + } + + protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> { + + @Override + protected Void doInBackground(Object... params) { + switch ((Integer)params[0]) { + case MESSAGE_CLEAR: + clearCacheInternal(); + break; + case MESSAGE_INIT_DISK_CACHE: + initDiskCacheInternal(); + break; + case MESSAGE_FLUSH: + flushCacheInternal(); + break; + case MESSAGE_CLOSE: + closeCacheInternal(); + break; + } + return null; + } + } + + protected void initDiskCacheInternal() { + if (mImageCache != null) { + mImageCache.initDiskCache(); + } + } + + protected void clearCacheInternal() { + if (mImageCache != null) { + mImageCache.clearCache(); + } + } + + protected void flushCacheInternal() { + if (mImageCache != null) { + mImageCache.flush(); + } + } + + protected void closeCacheInternal() { + if (mImageCache != null) { + mImageCache.close(); + mImageCache = null; + } + } + + public void clearCache() { + new CacheAsyncTask().execute(MESSAGE_CLEAR); + } + + public void flushCache() { + new CacheAsyncTask().execute(MESSAGE_FLUSH); + } + + public void closeCache() { + new CacheAsyncTask().execute(MESSAGE_CLOSE); + } +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/RecyclingBitmapDrawable.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/RecyclingBitmapDrawable.java new file mode 100644 index 000000000..5534a6aea --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/RecyclingBitmapDrawable.java @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.displayingbitmaps.util; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; + +import com.example.android.common.logger.Log; +import com.example.android.displayingbitmaps.BuildConfig; + +/** + * A BitmapDrawable that keeps track of whether it is being displayed or cached. + * When the drawable is no longer being displayed or cached, + * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap. + */ +public class RecyclingBitmapDrawable extends BitmapDrawable { + + static final String TAG = "CountingBitmapDrawable"; + + private int mCacheRefCount = 0; + private int mDisplayRefCount = 0; + + private boolean mHasBeenDisplayed; + + public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + /** + * Notify the drawable that the displayed state has changed. Internally a + * count is kept so that the drawable knows when it is no longer being + * displayed. + * + * @param isDisplayed - Whether the drawable is being displayed or not + */ + public void setIsDisplayed(boolean isDisplayed) { + //BEGIN_INCLUDE(set_is_displayed) + synchronized (this) { + if (isDisplayed) { + mDisplayRefCount++; + mHasBeenDisplayed = true; + } else { + mDisplayRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + //END_INCLUDE(set_is_displayed) + } + + /** + * Notify the drawable that the cache state has changed. Internally a count + * is kept so that the drawable knows when it is no longer being cached. + * + * @param isCached - Whether the drawable is being cached or not + */ + public void setIsCached(boolean isCached) { + //BEGIN_INCLUDE(set_is_cached) + synchronized (this) { + if (isCached) { + mCacheRefCount++; + } else { + mCacheRefCount--; + } + } + + // Check to see if recycle() can be called + checkState(); + //END_INCLUDE(set_is_cached) + } + + private synchronized void checkState() { + //BEGIN_INCLUDE(check_state) + // If the drawable cache and display ref counts = 0, and this drawable + // has been displayed, then recycle + if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed + && hasValidBitmap()) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "No longer being used or cached so recycling. " + + toString()); + } + + getBitmap().recycle(); + } + //END_INCLUDE(check_state) + } + + private synchronized boolean hasValidBitmap() { + Bitmap bitmap = getBitmap(); + return bitmap != null && !bitmap.isRecycled(); + } + +} diff --git a/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/Utils.java b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/Utils.java new file mode 100644 index 000000000..505d0efb1 --- /dev/null +++ b/samples/browseable/DisplayingBitmaps/src/com.example.android.displayingbitmaps/util/Utils.java @@ -0,0 +1,82 @@ +/* + * 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.example.android.displayingbitmaps.util; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.StrictMode; + +import com.example.android.displayingbitmaps.ui.ImageDetailActivity; +import com.example.android.displayingbitmaps.ui.ImageGridActivity; + +/** + * Class containing some static utility methods. + */ +public class Utils { + private Utils() {}; + + + @TargetApi(VERSION_CODES.HONEYCOMB) + public static void enableStrictMode() { + if (Utils.hasGingerbread()) { + StrictMode.ThreadPolicy.Builder threadPolicyBuilder = + new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog(); + StrictMode.VmPolicy.Builder vmPolicyBuilder = + new StrictMode.VmPolicy.Builder() + .detectAll() + .penaltyLog(); + + if (Utils.hasHoneycomb()) { + threadPolicyBuilder.penaltyFlashScreen(); + vmPolicyBuilder + .setClassInstanceLimit(ImageGridActivity.class, 1) + .setClassInstanceLimit(ImageDetailActivity.class, 1); + } + StrictMode.setThreadPolicy(threadPolicyBuilder.build()); + StrictMode.setVmPolicy(vmPolicyBuilder.build()); + } + } + + public static boolean hasFroyo() { + // Can use static final constants like FROYO, declared in later versions + // of the OS since they are inlined at compile time. This is guaranteed behavior. + return Build.VERSION.SDK_INT >= VERSION_CODES.FROYO; + } + + public static boolean hasGingerbread() { + return Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD; + } + + public static boolean hasHoneycomb() { + return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + } + + public static boolean hasHoneycombMR1() { + return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + } + + public static boolean hasJellyBean() { + return Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + } + + public static boolean hasKitKat() { + return Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT; + } +} diff --git a/samples/browseable/MediaRouter/AndroidManifest.xml b/samples/browseable/MediaRouter/AndroidManifest.xml new file mode 100644 index 000000000..0b5bec450 --- /dev/null +++ b/samples/browseable/MediaRouter/AndroidManifest.xml @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Declare the contents of this Android application. The namespace + attribute brings in the Android platform namespace, and the package + supplies a unique name for the application. When writing your + own application, the package name must be changed from "com.example.*" + to come from a domain that you own or have control over. --> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.mediarouter"> + <!-- Permission for INTERNET is required for streaming video content + from the web, it's not required otherwise. --> + <uses-permission android:name="android.permission.INTERNET" /> + <!-- Permission for SYSTEM_ALERT_WINDOW is only required for emulating + remote display using system alert window. --> + <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> + + <uses-sdk android:targetSdkVersion="19" + android:minSdkVersion="7"/> + + <!-- The smallest screen this app works on is a phone. The app will + scale its UI to larger screens but doesn't make good use of them + so allow the compatibility mode button to be shown (mostly because + this is just convenient for testing). --> + <supports-screens android:requiresSmallestWidthDp="320" + android:compatibleWidthLimitDp="480" /> + + <application android:label="@string/app_name" + android:icon="@drawable/ic_launcher" + android:hardwareAccelerated="true"> + + <receiver android:name=".player.SampleMediaButtonReceiver"> + <intent-filter> + <action android:name="android.intent.action.MEDIA_BUTTON" /> + </intent-filter> + </receiver> + <!-- MediaRouter Support Samples --> + + <activity android:name=".player.MainActivity" + android:label="@string/app_name" + android:theme="@style/Theme.AppCompat.Light"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="com.example.android.supportv7.SAMPLE_CODE" /> + </intent-filter> + </activity> + + <service android:name=".provider.SampleMediaRouteProviderService" + android:label="@string/sample_media_route_provider_service" + android:process=":mrp"> + <intent-filter> + <action android:name="android.media.MediaRouteProviderService" /> + </intent-filter> + </service> + + </application> +</manifest> diff --git a/samples/browseable/MediaRouter/_index.jd b/samples/browseable/MediaRouter/_index.jd new file mode 100644 index 000000000..a46cae8dc --- /dev/null +++ b/samples/browseable/MediaRouter/_index.jd @@ -0,0 +1,10 @@ + + + +page.tags="MediaRouter" +sample.group=Media +@jd:body + +<p> + Demonstrates how to create a custom media route provider. +</p> diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_pause.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_pause.png Binary files differnew file mode 100644 index 000000000..d30ba3cd1 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_play.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_play.png Binary files differnew file mode 100644 index 000000000..869f0014b --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_stop.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_stop.png Binary files differnew file mode 100644 index 000000000..2b6c0f40a --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_action_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_launcher.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..d34011451 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_launcher.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_pause.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_pause.png Binary files differnew file mode 100644 index 000000000..1d465a41e --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_play.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_play.png Binary files differnew file mode 100644 index 000000000..2746d17fb --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_stop.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_stop.png Binary files differnew file mode 100644 index 000000000..a0ff13695 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_media_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_add.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..444e8a5ee --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_add.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_delete.png b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_delete.png Binary files differnew file mode 100644 index 000000000..24d8f6a4b --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/ic_menu_delete.png diff --git a/samples/browseable/MediaRouter/res/drawable-hdpi/tile.9.png b/samples/browseable/MediaRouter/res/drawable-hdpi/tile.9.png Binary files differnew file mode 100644 index 000000000..135862883 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-hdpi/tile.9.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_pause.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_pause.png Binary files differnew file mode 100644 index 000000000..2c96c7b84 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_play.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_play.png Binary files differnew file mode 100644 index 000000000..5f3bf86fd --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_stop.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_stop.png Binary files differnew file mode 100644 index 000000000..ddaf37a26 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_action_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_launcher.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..6af9981da --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_launcher.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_pause.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_pause.png Binary files differnew file mode 100644 index 000000000..3e6b2a17b --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_play.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_play.png Binary files differnew file mode 100644 index 000000000..7966bbc51 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_stop.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_stop.png Binary files differnew file mode 100644 index 000000000..8ea7efee5 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_media_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_add.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_add.png Binary files differnew file mode 100644 index 000000000..361c7c460 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_add.png diff --git a/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_delete.png b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_delete.png Binary files differnew file mode 100644 index 000000000..e2c8700c3 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-mdpi/ic_menu_delete.png diff --git a/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_pause.png b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_pause.png Binary files differnew file mode 100644 index 000000000..504389aa0 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_play.png b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_play.png Binary files differnew file mode 100644 index 000000000..7f709bbf1 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_stop.png b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_stop.png Binary files differnew file mode 100644 index 000000000..2b07de4c2 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_action_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_launcher.png b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..16bd61245 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xhdpi/ic_launcher.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_pause.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_pause.png Binary files differnew file mode 100644 index 000000000..c3b376a12 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_pause.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_play.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_play.png Binary files differnew file mode 100644 index 000000000..df5994710 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_play.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_stop.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_stop.png Binary files differnew file mode 100644 index 000000000..f42d5252a --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_action_stop.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_launcher.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100755 index 000000000..4b1d92015 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_launcher.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_add.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_add.png Binary files differnew file mode 100644 index 000000000..b880d40ea --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_add.png diff --git a/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_delete.png b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_delete.png Binary files differnew file mode 100644 index 000000000..f9e27021f --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable-xxhdpi/ic_suggestions_delete.png diff --git a/samples/browseable/MediaRouter/res/drawable/list_background.xml b/samples/browseable/MediaRouter/res/drawable/list_background.xml new file mode 100644 index 000000000..be7240bb6 --- /dev/null +++ b/samples/browseable/MediaRouter/res/drawable/list_background.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:drawable="@color/list_highlight_color" /> + <item + android:drawable="@android:color/transparent" /> +</selector> diff --git a/samples/browseable/MediaRouter/res/layout-land/grid_layout_2.xml b/samples/browseable/MediaRouter/res/layout-land/grid_layout_2.xml new file mode 100644 index 000000000..da161230a --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout-land/grid_layout_2.xml @@ -0,0 +1,63 @@ +<?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. +--> + +<android.support.v7.widget.GridLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:useDefaultMargins="true" + android:alignmentMode="alignBounds" + android:rowOrderPreserved="false" + android:columnCount="4" + > + <TextView + android:text="Email setup" + android:textSize="32dip" + android:layout_columnSpan="4" + android:layout_gravity="center_horizontal" + /> + <TextView + android:text="You can configure email in a few simple steps:" + android:textSize="16dip" + android:layout_columnSpan="4" + android:layout_gravity="left" + /> + <TextView + android:text="Email address:" + android:layout_gravity="right" + /> + <EditText + android:ems="10" + /> + <TextView + android:text="Password:" + android:layout_column="0" + android:layout_gravity="right" + /> + <EditText + android:ems="8" + /> + <Button + android:text="Manual setup" + android:layout_row="5" + android:layout_column="3" + /> + <Button + android:text="Next" + android:layout_column="3" + android:layout_gravity="fill_horizontal" + /> +</android.support.v7.widget.GridLayout> diff --git a/samples/browseable/MediaRouter/res/layout/activity_main.xml b/samples/browseable/MediaRouter/res/layout/activity_main.xml new file mode 100755 index 000000000..be1aa49d9 --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout/activity_main.xml @@ -0,0 +1,36 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout style="@style/Widget.SampleMessageTile" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView style="@style/Widget.SampleMessage" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="@dimen/horizontal_page_margin" + android:layout_marginRight="@dimen/horizontal_page_margin" + android:layout_marginTop="@dimen/vertical_page_margin" + android:layout_marginBottom="@dimen/vertical_page_margin" + android:text="@string/intro_message" /> + </LinearLayout> +</LinearLayout> diff --git a/samples/browseable/MediaRouter/res/layout/media_item.xml b/samples/browseable/MediaRouter/res/layout/media_item.xml new file mode 100644 index 000000000..3a148615f --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout/media_item.xml @@ -0,0 +1,45 @@ +<?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. +--> + +<!-- Layout for list item in Library or Playlist view. Displays ImageButton + instead of radio button to the right of the item. --> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:clickable="true" + android:background="@drawable/list_background" + android:gravity="center_vertical"> + + <ImageButton android:id="@+id/item_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="48dp" + android:minHeight="48dp" + android:layout_alignParentRight="true" + android:layout_centerVertical="true" + android:background="@null"/> + + <TextView android:id="@+id/item_text" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceMedium" + android:layout_centerVertical="true" + android:layout_toLeftOf="@id/item_action" + android:layout_gravity="left" + android:gravity="left"/> +</RelativeLayout> diff --git a/samples/browseable/MediaRouter/res/layout/overlay_display_window.xml b/samples/browseable/MediaRouter/res/layout/overlay_display_window.xml new file mode 100644 index 000000000..36b4a0da2 --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout/overlay_display_window.xml @@ -0,0 +1,28 @@ +<?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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#000000"> + <TextureView android:id="@+id/overlay_display_window_texture" + android:layout_width="0px" + android:layout_height="0px" /> + <TextView android:id="@+id/overlay_display_window_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|center_horizontal" /> +</FrameLayout> diff --git a/samples/browseable/MediaRouter/res/layout/sample_media_router.xml b/samples/browseable/MediaRouter/res/layout/sample_media_router.xml new file mode 100644 index 000000000..161841801 --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout/sample_media_router.xml @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- See corresponding Java code MainActivity.java. --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical"> + <!-- Tabs for media library, playlist and statistics --> + <TabHost android:id="@+id/tabHost" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:layout_weight="1"> + <LinearLayout + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + <TabWidget android:id="@android:id/tabs" + android:layout_width="fill_parent" + android:layout_height="wrap_content" /> + + <FrameLayout android:id="@android:id/tabcontent" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + <LinearLayout android:id="@+id/tab1" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + <ListView android:id="@+id/media" + android:listSelector="@drawable/list_background" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" /> + </LinearLayout> + + <LinearLayout android:id="@+id/tab2" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> + <ListView android:id="@+id/playlist" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1"/> + </LinearLayout> + + <LinearLayout android:id="@+id/tab3" + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> + <TextView android:id="@+id/info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:textAppearance="?android:attr/textAppearanceMedium"/> + </LinearLayout> + </FrameLayout> + </LinearLayout> + </TabHost> + + <!-- Control buttons for the currently selected route. --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0"> + + <SeekBar android:id="@+id/seekbar" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + style="?android:attr/progressBarStyleHorizontal" + android:max="100" + android:progress="0" + android:layout_gravity="center" + android:layout_weight="1"/> + + <ImageButton android:id="@+id/pause_resume_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_gravity="right" + android:minWidth="48dp" + android:minHeight="48dp" + android:background="@null" + android:src="@drawable/ic_action_pause" /> + + <ImageButton android:id="@+id/stop_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="0" + android:layout_gravity="right" + android:minWidth="48dp" + android:minHeight="48dp" + android:background="@null" + android:src="@drawable/ic_action_stop" /> + </LinearLayout> + + </LinearLayout> + + <!-- Some content for visual interest in the case where no presentation is showing. --> + <FrameLayout android:id="@+id/player" + android:background="#ff000000" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center"> + <SurfaceView android:id="@+id/surface_view" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + </LinearLayout> + <TextView + android:textColor="#ffaaaaaa" + android:text="@string/sample_media_route_activity_local" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|center_horizontal" /> + </FrameLayout> +</LinearLayout> diff --git a/samples/browseable/MediaRouter/res/layout/sample_media_router_presentation.xml b/samples/browseable/MediaRouter/res/layout/sample_media_router_presentation.xml new file mode 100644 index 000000000..f029627d2 --- /dev/null +++ b/samples/browseable/MediaRouter/res/layout/sample_media_router_presentation.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- The content that we show on secondary displays. + See corresponding Java code PresentationWithMediaRouterActivity.java. --> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#ff000000"> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center"> + <SurfaceView android:id="@+id/surface_view" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + </LinearLayout> + <TextView + android:textColor="#ffaaaaaa" + android:text="@string/sample_media_route_activity_presentation" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|center_horizontal" /> +</FrameLayout> diff --git a/samples/browseable/MediaRouter/res/menu/sample_media_router_menu.xml b/samples/browseable/MediaRouter/res/menu/sample_media_router_menu.xml new file mode 100644 index 000000000..8057fa800 --- /dev/null +++ b/samples/browseable/MediaRouter/res/menu/sample_media_router_menu.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 Google Inc. + + 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" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item android:id="@+id/media_route_menu_item" + android:title="@string/media_route_menu_title" + app:showAsAction="always" + app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"/> +</menu> diff --git a/samples/browseable/MediaRouter/res/values-sw600dp/template-dimens.xml b/samples/browseable/MediaRouter/res/values-sw600dp/template-dimens.xml new file mode 100644 index 000000000..22074a2bd --- /dev/null +++ b/samples/browseable/MediaRouter/res/values-sw600dp/template-dimens.xml @@ -0,0 +1,24 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_huge</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/MediaRouter/res/values-sw600dp/template-styles.xml b/samples/browseable/MediaRouter/res/values-sw600dp/template-styles.xml new file mode 100644 index 000000000..03d197418 --- /dev/null +++ b/samples/browseable/MediaRouter/res/values-sw600dp/template-styles.xml @@ -0,0 +1,25 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceLarge</item> + <item name="android:lineSpacingMultiplier">1.2</item> + <item name="android:shadowDy">-6.5</item> + </style> + +</resources> diff --git a/samples/browseable/MediaRouter/res/values/arrays.xml b/samples/browseable/MediaRouter/res/values/arrays.xml new file mode 100644 index 000000000..8d658eb1e --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/arrays.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <string-array name="media_names"> + <item>Big Buck Bunny</item> + <item>Elephants Dream</item> + <item>Sintel</item> + <item>Tears of Steel</item> + </string-array> + + <string-array name="media_uris"> + <item>http://archive.org/download/BigBuckBunny_328/BigBuckBunny_512kb.mp4</item> + <item>http://archive.org/download/ElephantsDream_277/elephant_dreams_640_512kb.mp4</item> + <item>http://archive.org/download/Sintel/sintel-2048-stereo_512kb.mp4</item> + <item>http://archive.org/download/Tears-of-Steel/tears_of_steel_720p.mp4</item> + </string-array> +</resources> diff --git a/samples/browseable/MediaRouter/res/values/base-strings.xml b/samples/browseable/MediaRouter/res/values/base-strings.xml new file mode 100644 index 000000000..8b494cc82 --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/base-strings.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + + + +<resources> + <string name="app_name">MediaRouter</string> + <string name="intro_message"> + <![CDATA[ + + Demonstrates how to create a custom media route provider. + + ]]> + </string> +</resources> diff --git a/samples/browseable/MediaRouter/res/values/colors.xml b/samples/browseable/MediaRouter/res/values/colors.xml new file mode 100644 index 000000000..13cf3b5b9 --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/colors.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2007 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> + <drawable name="blue">#770000ff</drawable> + <color name="list_highlight_color">#ccc</color> +</resources> diff --git a/samples/browseable/MediaRouter/res/values/strings.xml b/samples/browseable/MediaRouter/res/values/strings.xml new file mode 100644 index 000000000..c750ce15f --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/strings.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + + <string name="sample_media_router_text">This activity demonstrates how to + use MediaRouter from the support library. Select a route from the action bar.</string> + <string name="media_route_menu_title">Play on...</string> + + <string name="library_tab_text">Library</string> + <string name="playlist_tab_text">Playlist</string> + <string name="statistics_tab_text">Statistics</string> + + <string name="sample_media_route_provider_service">Media Route Provider Service Support Library Sample</string> + <string name="fixed_volume_route_name">Fixed Volume Remote Playback Route</string> + <string name="variable_volume_basic_route_name">Variable Volume (Basic) Remote Playback Route</string> + <string name="variable_volume_queuing_route_name">Variable Volume (Queuing) Remote Playback Route</string> + <string name="variable_volume_session_route_name">Variable Volume (Session) Remote Playback Route</string> + <string name="sample_route_description">Sample route</string> + + <string name="sample_media_route_provider_remote">Remote Playback (Simulated)</string> + <string name="sample_media_route_activity_local">Local Playback</string> + <string name="sample_media_route_activity_presentation">Local Playback on Presentation Display</string> + <string name="playlist_item_added_text">Added to playlist!</string> + <string name="playlist_item_removed_text">Removed from playlist</string> + +</resources> diff --git a/samples/browseable/MediaRouter/res/values/template-dimens.xml b/samples/browseable/MediaRouter/res/values/template-dimens.xml new file mode 100644 index 000000000..39e710b5c --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/template-dimens.xml @@ -0,0 +1,32 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Define standard dimensions to comply with Holo-style grids and rhythm. --> + + <dimen name="margin_tiny">4dp</dimen> + <dimen name="margin_small">8dp</dimen> + <dimen name="margin_medium">16dp</dimen> + <dimen name="margin_large">32dp</dimen> + <dimen name="margin_huge">64dp</dimen> + + <!-- Semantic definitions --> + + <dimen name="horizontal_page_margin">@dimen/margin_medium</dimen> + <dimen name="vertical_page_margin">@dimen/margin_medium</dimen> + +</resources> diff --git a/samples/browseable/MediaRouter/res/values/template-styles.xml b/samples/browseable/MediaRouter/res/values/template-styles.xml new file mode 100644 index 000000000..404623e3d --- /dev/null +++ b/samples/browseable/MediaRouter/res/values/template-styles.xml @@ -0,0 +1,42 @@ +<!-- + Copyright 2013 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + --> + +<resources> + + <!-- Activity themes --> + + <style name="Theme.Base" parent="android:Theme.Holo.Light" /> + + <style name="Theme.Sample" parent="Theme.Base" /> + + <style name="AppTheme" parent="Theme.Sample" /> + <!-- Widget styling --> + + <style name="Widget" /> + + <style name="Widget.SampleMessage"> + <item name="android:textAppearance">?android:textAppearanceMedium</item> + <item name="android:lineSpacingMultiplier">1.1</item> + </style> + + <style name="Widget.SampleMessageTile"> + <item name="android:background">@drawable/tile</item> + <item name="android:shadowColor">#7F000000</item> + <item name="android:shadowDy">-3.5</item> + <item name="android:shadowRadius">2</item> + </style> + +</resources> diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java new file mode 100644 index 000000000..17503c568 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/Log.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Helper class for a list (or tree) of LoggerNodes. + * + * <p>When this is set as the head of the list, + * an instance of it can function as a drop-in replacement for {@link android.util.Log}. + * Most of the methods in this class server only to map a method call in Log to its equivalent + * in LogNode.</p> + */ +public class Log { + // Grabbing the native values from Android's native logging facilities, + // to make for easy migration and interop. + public static final int NONE = -1; + public static final int VERBOSE = android.util.Log.VERBOSE; + public static final int DEBUG = android.util.Log.DEBUG; + public static final int INFO = android.util.Log.INFO; + public static final int WARN = android.util.Log.WARN; + public static final int ERROR = android.util.Log.ERROR; + public static final int ASSERT = android.util.Log.ASSERT; + + // Stores the beginning of the LogNode topology. + private static LogNode mLogNode; + + /** + * Returns the next LogNode in the linked list. + */ + public static LogNode getLogNode() { + return mLogNode; + } + + /** + * Sets the LogNode data will be sent to. + */ + public static void setLogNode(LogNode node) { + mLogNode = node; + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void println(int priority, String tag, String msg, Throwable tr) { + if (mLogNode != null) { + mLogNode.println(priority, tag, msg, tr); + } + } + + /** + * Instructs the LogNode to print the log data provided. Other LogNodes can + * be chained to the end of the LogNode as desired. + * + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + */ + public static void println(int priority, String tag, String msg) { + println(priority, tag, msg, null); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void v(String tag, String msg, Throwable tr) { + println(VERBOSE, tag, msg, tr); + } + + /** + * Prints a message at VERBOSE priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void v(String tag, String msg) { + v(tag, msg, null); + } + + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void d(String tag, String msg, Throwable tr) { + println(DEBUG, tag, msg, tr); + } + + /** + * Prints a message at DEBUG priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void d(String tag, String msg) { + d(tag, msg, null); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void i(String tag, String msg, Throwable tr) { + println(INFO, tag, msg, tr); + } + + /** + * Prints a message at INFO priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void i(String tag, String msg) { + i(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, String msg, Throwable tr) { + println(WARN, tag, msg, tr); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void w(String tag, String msg) { + w(tag, msg, null); + } + + /** + * Prints a message at WARN priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void w(String tag, Throwable tr) { + w(tag, null, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void e(String tag, String msg, Throwable tr) { + println(ERROR, tag, msg, tr); + } + + /** + * Prints a message at ERROR priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void e(String tag, String msg) { + e(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, String msg, Throwable tr) { + println(ASSERT, tag, msg, tr); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. + */ + public static void wtf(String tag, String msg) { + wtf(tag, msg, null); + } + + /** + * Prints a message at ASSERT priority. + * + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public static void wtf(String tag, Throwable tr) { + wtf(tag, null, tr); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java new file mode 100644 index 000000000..b302acd4b --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogFragment.java @@ -0,0 +1,109 @@ +/* +* Copyright 2013 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +/* + * Copyright 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.common.logger; + +import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; + +/** + * Simple fraggment which contains a LogView and uses is to output log data it receives + * through the LogNode interface. + */ +public class LogFragment extends Fragment { + + private LogView mLogView; + private ScrollView mScrollView; + + public LogFragment() {} + + public View inflateViews() { + mScrollView = new ScrollView(getActivity()); + ViewGroup.LayoutParams scrollParams = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mScrollView.setLayoutParams(scrollParams); + + mLogView = new LogView(getActivity()); + ViewGroup.LayoutParams logParams = new ViewGroup.LayoutParams(scrollParams); + logParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + mLogView.setLayoutParams(logParams); + mLogView.setClickable(true); + mLogView.setFocusable(true); + mLogView.setTypeface(Typeface.MONOSPACE); + + // Want to set padding as 16 dips, setPadding takes pixels. Hooray math! + int paddingDips = 16; + double scale = getResources().getDisplayMetrics().density; + int paddingPixels = (int) ((paddingDips * (scale)) + .5); + mLogView.setPadding(paddingPixels, paddingPixels, paddingPixels, paddingPixels); + mLogView.setCompoundDrawablePadding(paddingPixels); + + mLogView.setGravity(Gravity.BOTTOM); + mLogView.setTextAppearance(getActivity(), android.R.style.TextAppearance_Holo_Medium); + + mScrollView.addView(mLogView); + return mScrollView; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + + View result = inflateViews(); + + mLogView.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + mScrollView.fullScroll(ScrollView.FOCUS_DOWN); + } + }); + return result; + } + + public LogView getLogView() { + return mLogView; + } +}
\ No newline at end of file diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java new file mode 100644 index 000000000..bc37cabc0 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogNode.java @@ -0,0 +1,39 @@ +/* + * 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.example.android.common.logger; + +/** + * Basic interface for a logging system that can output to one or more targets. + * Note that in addition to classes that will output these logs in some format, + * one can also implement this interface over a filter and insert that in the chain, + * such that no targets further down see certain data, or see manipulated forms of the data. + * You could, for instance, write a "ToHtmlLoggerNode" that just converted all the log data + * it received to HTML and sent it along to the next node in the chain, without printing it + * anywhere. + */ +public interface LogNode { + + /** + * Instructs first LogNode in the list to print the log data provided. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + public void println(int priority, String tag, String msg, Throwable tr); + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java new file mode 100644 index 000000000..c01542b91 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogView.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +import android.app.Activity; +import android.content.Context; +import android.util.*; +import android.widget.TextView; + +/** Simple TextView which is used to output log data received through the LogNode interface. +*/ +public class LogView extends TextView implements LogNode { + + public LogView(Context context) { + super(context); + } + + public LogView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LogView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Formats the log data and prints it out to the LogView. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + + + String priorityStr = null; + + // For the purposes of this View, we want to print the priority as readable text. + switch(priority) { + case android.util.Log.VERBOSE: + priorityStr = "VERBOSE"; + break; + case android.util.Log.DEBUG: + priorityStr = "DEBUG"; + break; + case android.util.Log.INFO: + priorityStr = "INFO"; + break; + case android.util.Log.WARN: + priorityStr = "WARN"; + break; + case android.util.Log.ERROR: + priorityStr = "ERROR"; + break; + case android.util.Log.ASSERT: + priorityStr = "ASSERT"; + break; + default: + break; + } + + // Handily, the Log class has a facility for converting a stack trace into a usable string. + String exceptionStr = null; + if (tr != null) { + exceptionStr = android.util.Log.getStackTraceString(tr); + } + + // Take the priority, tag, message, and exception, and concatenate as necessary + // into one usable line of text. + final StringBuilder outputBuilder = new StringBuilder(); + + String delimiter = "\t"; + appendIfNotNull(outputBuilder, priorityStr, delimiter); + appendIfNotNull(outputBuilder, tag, delimiter); + appendIfNotNull(outputBuilder, msg, delimiter); + appendIfNotNull(outputBuilder, exceptionStr, delimiter); + + // In case this was originally called from an AsyncTask or some other off-UI thread, + // make sure the update occurs within the UI thread. + ((Activity) getContext()).runOnUiThread( (new Thread(new Runnable() { + @Override + public void run() { + // Display the text we just generated within the LogView. + appendToLog(outputBuilder.toString()); + } + }))); + + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } + + public LogNode getNext() { + return mNext; + } + + public void setNext(LogNode node) { + mNext = node; + } + + /** Takes a string and adds to it, with a separator, if the bit to be added isn't null. Since + * the logger takes so many arguments that might be null, this method helps cut out some of the + * agonizing tedium of writing the same 3 lines over and over. + * @param source StringBuilder containing the text to append to. + * @param addStr The String to append + * @param delimiter The String to separate the source and appended strings. A tab or comma, + * for instance. + * @return The fully concatenated String as a StringBuilder + */ + private StringBuilder appendIfNotNull(StringBuilder source, String addStr, String delimiter) { + if (addStr != null) { + if (addStr.length() == 0) { + delimiter = ""; + } + + return source.append(addStr).append(delimiter); + } + return source; + } + + // The next LogNode in the chain. + LogNode mNext; + + /** Outputs the string as a new line of log data in the LogView. */ + public void appendToLog(String s) { + append("\n" + s); + } + + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java new file mode 100644 index 000000000..16a9e7ba2 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/LogWrapper.java @@ -0,0 +1,75 @@ +/* + * 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.example.android.common.logger; + +import android.util.Log; + +/** + * Helper class which wraps Android's native Log utility in the Logger interface. This way + * normal DDMS output can be one of the many targets receiving and outputting logs simultaneously. + */ +public class LogWrapper implements LogNode { + + // For piping: The next node to receive Log data after this one has done its work. + private LogNode mNext; + + /** + * Returns the next LogNode in the linked list. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + + /** + * Prints data out to the console using Android's native log mechanism. + * @param priority Log level of the data being logged. Verbose, Error, etc. + * @param tag Tag for for the log data. Can be used to organize log statements. + * @param msg The actual message to be logged. The actual message to be logged. + * @param tr If an exception was thrown, this can be sent along for the logging facilities + * to extract and print useful information. + */ + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + // There actually are log methods that don't take a msg parameter. For now, + // if that's the case, just convert null to the empty string and move on. + String useMsg = msg; + if (useMsg == null) { + useMsg = ""; + } + + // If an exeption was provided, convert that exception to a usable string and attach + // it to the end of the msg method. + if (tr != null) { + msg += "\n" + Log.getStackTraceString(tr); + } + + // This is functionally identical to Log.x(tag, useMsg); + // For instance, if priority were Log.VERBOSE, this would be the same as Log.v(tag, useMsg) + Log.println(priority, tag, useMsg); + + // If this isn't the last node in the chain, move things along. + if (mNext != null) { + mNext.println(priority, tag, msg, tr); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java b/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java new file mode 100644 index 000000000..19967dcd4 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.common.logger/MessageOnlyLogFilter.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.android.common.logger; + +/** + * Simple {@link LogNode} filter, removes everything except the message. + * Useful for situations like on-screen log output where you don't want a lot of metadata displayed, + * just easy-to-read message updates as they're happening. + */ +public class MessageOnlyLogFilter implements LogNode { + + LogNode mNext; + + /** + * Takes the "next" LogNode as a parameter, to simplify chaining. + * + * @param next The next LogNode in the pipeline. + */ + public MessageOnlyLogFilter(LogNode next) { + mNext = next; + } + + public MessageOnlyLogFilter() { + } + + @Override + public void println(int priority, String tag, String msg, Throwable tr) { + if (mNext != null) { + getNext().println(Log.NONE, null, msg, null); + } + } + + /** + * Returns the next LogNode in the chain. + */ + public LogNode getNext() { + return mNext; + } + + /** + * Sets the LogNode data will be sent to.. + */ + public void setNext(LogNode node) { + mNext = node; + } + +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java new file mode 100644 index 000000000..86f51001d --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/LocalPlayer.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.Activity; +import android.app.Presentation; +import android.content.Context; +import android.content.DialogInterface; +import android.media.MediaPlayer; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; +import android.view.Display; +import android.view.Gravity; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import com.example.android.mediarouter.R; + +import java.io.IOException; + +/** + * Handles playback of a single media item using MediaPlayer. + */ +public abstract class LocalPlayer extends Player implements + MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener, + MediaPlayer.OnSeekCompleteListener { + private static final String TAG = "LocalPlayer"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final int STATE_IDLE = 0; + private static final int STATE_PLAY_PENDING = 1; + private static final int STATE_READY = 2; + private static final int STATE_PLAYING = 3; + private static final int STATE_PAUSED = 4; + + private final Context mContext; + private final Handler mHandler = new Handler(); + private MediaPlayer mMediaPlayer; + private int mState = STATE_IDLE; + private int mSeekToPos; + private int mVideoWidth; + private int mVideoHeight; + private Surface mSurface; + private SurfaceHolder mSurfaceHolder; + + public LocalPlayer(Context context) { + mContext = context; + + // reset media player + reset(); + } + + @Override + public boolean isRemotePlayback() { + return false; + } + + @Override + public boolean isQueuingSupported() { + return false; + } + + @Override + public void connect(RouteInfo route) { + if (DEBUG) { + Log.d(TAG, "connecting to: " + route); + } + } + + @Override + public void release() { + if (DEBUG) { + Log.d(TAG, "releasing"); + } + // release media player + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + } + + // Player + @Override + public void play(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "play: item=" + item); + } + reset(); + mSeekToPos = (int)item.getPosition(); + try { + mMediaPlayer.setDataSource(mContext, item.getUri()); + mMediaPlayer.prepareAsync(); + } catch (IllegalStateException e) { + Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + item.getUri()); + } catch (IOException e) { + Log.e(TAG, "MediaPlayer throws IOException, uri=" + item.getUri()); + } catch (IllegalArgumentException e) { + Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + item.getUri()); + } catch (SecurityException e) { + Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + item.getUri()); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + resume(); + } else { + pause(); + } + } + + @Override + public void seek(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "seek: item=" + item); + } + int pos = (int)item.getPosition(); + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.seekTo(pos); + mSeekToPos = pos; + } else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) { + // Seek before onPrepared() arrives, + // need to performed delayed seek in onPrepared() + mSeekToPos = pos; + } + } + + @Override + public void getStatus(final PlaylistItem item, final boolean update) { + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + // use mSeekToPos if we're currently seeking (mSeekToPos is reset + // when seeking is completed) + item.setDuration(mMediaPlayer.getDuration()); + item.setPosition(mSeekToPos > 0 ? + mSeekToPos : mMediaPlayer.getCurrentPosition()); + item.setTimestamp(SystemClock.elapsedRealtime()); + } + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + + @Override + public void pause() { + if (DEBUG) { + Log.d(TAG, "pause"); + } + if (mState == STATE_PLAYING) { + mMediaPlayer.pause(); + mState = STATE_PAUSED; + } + } + + @Override + public void resume() { + if (DEBUG) { + Log.d(TAG, "resume"); + } + if (mState == STATE_READY || mState == STATE_PAUSED) { + mMediaPlayer.start(); + mState = STATE_PLAYING; + } else if (mState == STATE_IDLE){ + mState = STATE_PLAY_PENDING; + } + } + + @Override + public void stop() { + if (DEBUG) { + Log.d(TAG, "stop"); + } + if (mState == STATE_PLAYING || mState == STATE_PAUSED) { + mMediaPlayer.stop(); + mState = STATE_IDLE; + } + } + + @Override + public void enqueue(final PlaylistItem item) { + throw new UnsupportedOperationException("LocalPlayer doesn't support enqueue!"); + } + + @Override + public PlaylistItem remove(String iid) { + throw new UnsupportedOperationException("LocalPlayer doesn't support remove!"); + } + + //MediaPlayer Listeners + @Override + public void onPrepared(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onPrepared"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mState == STATE_IDLE) { + mState = STATE_READY; + updateVideoRect(); + } else if (mState == STATE_PLAY_PENDING) { + mState = STATE_PLAYING; + updateVideoRect(); + if (mSeekToPos > 0) { + if (DEBUG) { + Log.d(TAG, "seek to initial pos: " + mSeekToPos); + } + mMediaPlayer.seekTo(mSeekToPos); + } + mMediaPlayer.start(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + @Override + public void onCompletion(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onCompletion"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onCompletion(); + } + } + }); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + if (DEBUG) { + Log.d(TAG, "onError"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + if (mCallback != null) { + mCallback.onError(); + } + } + }); + // return true so that onCompletion is not called + return true; + } + + @Override + public void onSeekComplete(MediaPlayer mp) { + if (DEBUG) { + Log.d(TAG, "onSeekComplete"); + } + mHandler.post(new Runnable() { + @Override + public void run() { + mSeekToPos = 0; + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + protected Context getContext() { return mContext; } + protected MediaPlayer getMediaPlayer() { return mMediaPlayer; } + protected int getVideoWidth() { return mVideoWidth; } + protected int getVideoHeight() { return mVideoHeight; } + protected void setSurface(Surface surface) { + mSurface = surface; + mSurfaceHolder = null; + updateSurface(); + } + + protected void setSurface(SurfaceHolder surfaceHolder) { + mSurface = null; + mSurfaceHolder = surfaceHolder; + updateSurface(); + } + + protected void removeSurface(SurfaceHolder surfaceHolder) { + if (surfaceHolder == mSurfaceHolder) { + setSurface((SurfaceHolder)null); + } + } + + protected void updateSurface() { + if (mMediaPlayer == null) { + // just return if media player is already gone + return; + } + if (mSurface != null) { + // The setSurface API does not exist until V14+. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + ICSMediaPlayer.setSurface(mMediaPlayer, mSurface); + } else { + throw new UnsupportedOperationException("MediaPlayer does not support " + + "setSurface() on this version of the platform."); + } + } else if (mSurfaceHolder != null) { + mMediaPlayer.setDisplay(mSurfaceHolder); + } else { + mMediaPlayer.setDisplay(null); + } + } + + protected abstract void updateSize(); + + private void reset() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnCompletionListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnSeekCompleteListener(this); + updateSurface(); + mState = STATE_IDLE; + mSeekToPos = 0; + } + + private void updateVideoRect() { + if (mState != STATE_IDLE && mState != STATE_PLAY_PENDING) { + int width = mMediaPlayer.getVideoWidth(); + int height = mMediaPlayer.getVideoHeight(); + if (width > 0 && height > 0) { + mVideoWidth = width; + mVideoHeight = height; + updateSize(); + } else { + Log.e(TAG, "video rect is 0x0!"); + mVideoWidth = mVideoHeight = 0; + } + } + } + + private static final class ICSMediaPlayer { + public static final void setSurface(MediaPlayer player, Surface surface) { + player.setSurface(surface); + } + } + + /** + * Handles playback of a single media item using MediaPlayer in SurfaceView + */ + public static class SurfaceViewPlayer extends LocalPlayer implements + SurfaceHolder.Callback { + private static final String TAG = "SurfaceViewPlayer"; + private RouteInfo mRoute; + private final SurfaceView mSurfaceView; + private final FrameLayout mLayout; + private DemoPresentation mPresentation; + + public SurfaceViewPlayer(Context context) { + super(context); + + mLayout = (FrameLayout)((Activity)context).findViewById(R.id.player); + mSurfaceView = (SurfaceView)((Activity)context).findViewById(R.id.surface_view); + + // add surface holder callback + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(this); + } + + @Override + public void connect(RouteInfo route) { + super.connect(route); + mRoute = route; + } + + @Override + public void release() { + super.release(); + + // dismiss presentation display + if (mPresentation != null) { + Log.i(TAG, "Dismissing presentation because the activity is no longer visible."); + mPresentation.dismiss(); + mPresentation = null; + } + + // remove surface holder callback + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.removeCallback(this); + + // hide the surface view when SurfaceViewPlayer is destroyed + mSurfaceView.setVisibility(View.GONE); + mLayout.setVisibility(View.GONE); + } + + @Override + public void updatePresentation() { + // Get the current route and its presentation display. + Display presentationDisplay = mRoute != null ? mRoute.getPresentationDisplay() : null; + + // Dismiss the current presentation if the display has changed. + if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) { + Log.i(TAG, "Dismissing presentation because the current route no longer " + + "has a presentation display."); + mPresentation.dismiss(); + mPresentation = null; + } + + // Show a new presentation if needed. + if (mPresentation == null && presentationDisplay != null) { + Log.i(TAG, "Showing presentation on display: " + presentationDisplay); + mPresentation = new DemoPresentation(getContext(), presentationDisplay); + mPresentation.setOnDismissListener(mOnDismissListener); + try { + mPresentation.show(); + } catch (WindowManager.InvalidDisplayException ex) { + Log.w(TAG, "Couldn't show presentation! Display was removed in " + + "the meantime.", ex); + mPresentation = null; + } + } + + updateContents(); + } + + // SurfaceHolder.Callback + @Override + public void surfaceChanged(SurfaceHolder holder, int format, + int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged: " + width + "x" + height); + } + setSurface(holder); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + if (DEBUG) { + Log.d(TAG, "surfaceCreated"); + } + setSurface(holder); + updateSize(); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + if (DEBUG) { + Log.d(TAG, "surfaceDestroyed"); + } + removeSurface(holder); + } + + @Override + protected void updateSize() { + int width = getVideoWidth(); + int height = getVideoHeight(); + if (width > 0 && height > 0) { + if (mPresentation == null) { + int surfaceWidth = mLayout.getWidth(); + int surfaceHeight = mLayout.getHeight(); + + // Calculate the new size of mSurfaceView, so that video is centered + // inside the framelayout with proper letterboxing/pillarboxing + ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams(); + if (surfaceWidth * height < surfaceHeight * width) { + // Black bars on top&bottom, mSurfaceView has full layout width, + // while height is derived from video's aspect ratio + lp.width = surfaceWidth; + lp.height = surfaceWidth * height / width; + } else { + // Black bars on left&right, mSurfaceView has full layout height, + // while width is derived from video's aspect ratio + lp.width = surfaceHeight * width / height; + lp.height = surfaceHeight; + } + Log.i(TAG, "video rect is " + lp.width + "x" + lp.height); + mSurfaceView.setLayoutParams(lp); + } else { + mPresentation.updateSize(width, height); + } + } + } + + private void updateContents() { + // Show either the content in the main activity or the content in the presentation + if (mPresentation != null) { + mLayout.setVisibility(View.GONE); + mSurfaceView.setVisibility(View.GONE); + } else { + mLayout.setVisibility(View.VISIBLE); + mSurfaceView.setVisibility(View.VISIBLE); + } + } + + // Listens for when presentations are dismissed. + private final DialogInterface.OnDismissListener mOnDismissListener = + new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + if (dialog == mPresentation) { + Log.i(TAG, "Presentation dismissed."); + mPresentation = null; + updateContents(); + } + } + }; + + // Presentation + private final class DemoPresentation extends Presentation { + private SurfaceView mPresentationSurfaceView; + + public DemoPresentation(Context context, Display display) { + super(context, display); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Be sure to call the super class. + super.onCreate(savedInstanceState); + + // Inflate the layout. + setContentView(R.layout.sample_media_router_presentation); + + // Set up the surface view. + mPresentationSurfaceView = (SurfaceView)findViewById(R.id.surface_view); + SurfaceHolder holder = mPresentationSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + holder.addCallback(SurfaceViewPlayer.this); + Log.i(TAG, "Presentation created"); + } + + public void updateSize(int width, int height) { + int surfaceHeight = getWindow().getDecorView().getHeight(); + int surfaceWidth = getWindow().getDecorView().getWidth(); + ViewGroup.LayoutParams lp = mPresentationSurfaceView.getLayoutParams(); + if (surfaceWidth * height < surfaceHeight * width) { + lp.width = surfaceWidth; + lp.height = surfaceWidth * height / width; + } else { + lp.width = surfaceHeight * width / height; + lp.height = surfaceHeight; + } + Log.i(TAG, "Presentation video rect is " + lp.width + "x" + lp.height); + mPresentationSurfaceView.setLayoutParams(lp); + } + } + } + + /** + * Handles playback of a single media item using MediaPlayer in + * OverlayDisplayWindow. + */ + public static class OverlayPlayer extends LocalPlayer implements + OverlayDisplayWindow.OverlayWindowListener { + private static final String TAG = "OverlayPlayer"; + private final OverlayDisplayWindow mOverlay; + + public OverlayPlayer(Context context) { + super(context); + + mOverlay = OverlayDisplayWindow.create(getContext(), + getContext().getResources().getString( + R.string.sample_media_route_provider_remote), + 1024, 768, Gravity.CENTER); + + mOverlay.setOverlayWindowListener(this); + } + + @Override + public void connect(RouteInfo route) { + super.connect(route); + mOverlay.show(); + } + + @Override + public void release() { + super.release(); + mOverlay.dismiss(); + } + + @Override + protected void updateSize() { + int width = getVideoWidth(); + int height = getVideoHeight(); + if (width > 0 && height > 0) { + mOverlay.updateAspectRatio(width, height); + } + } + + // OverlayDisplayWindow.OverlayWindowListener + @Override + public void onWindowCreated(Surface surface) { + setSurface(surface); + } + + @Override + public void onWindowCreated(SurfaceHolder surfaceHolder) { + setSurface(surfaceHolder); + } + + @Override + public void onWindowDestroyed() { + setSurface((SurfaceHolder)null); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java new file mode 100644 index 000000000..ad283dd24 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/MainActivity.java @@ -0,0 +1,724 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.media.AudioManager.OnAudioFocusChangeListener; +import android.media.RemoteControlClient; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.MediaRouteActionProvider; +import android.support.v7.app.MediaRouteDiscoveryFragment; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; +import android.support.v7.media.MediaRouter.Callback; +import android.support.v7.media.MediaRouter.ProviderInfo; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ArrayAdapter; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TabHost; +import android.widget.TabHost.OnTabChangeListener; +import android.widget.TabHost.TabSpec; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.android.mediarouter.R; +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +import java.io.File; + +/** + * <h3>Media Router Support Activity</h3> + * <p/> + * <p> + * This demonstrates how to use the {@link MediaRouter} API to build an + * application that allows the user to send content to various rendering + * targets. + * </p> + */ +public class MainActivity extends ActionBarActivity { + private static final String TAG = "MainActivity"; + private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment"; + + private MediaRouter mMediaRouter; + private MediaRouteSelector mSelector; + private LibraryAdapter mLibraryItems; + private PlaylistAdapter mPlayListItems; + private TextView mInfoTextView; + private ListView mLibraryView; + private ListView mPlayListView; + private ImageButton mPauseResumeButton; + private ImageButton mStopButton; + private SeekBar mSeekBar; + private boolean mPaused; + private boolean mNeedResume; + private boolean mSeeking; + + private RemoteControlClient mRemoteControlClient; + private ComponentName mEventReceiver; + private AudioManager mAudioManager; + private PendingIntent mMediaPendingIntent; + + private final Handler mHandler = new Handler(); + private final Runnable mUpdateSeekRunnable = new Runnable() { + @Override + public void run() { + updateProgress(); + // update UI every 1 second + mHandler.postDelayed(this, 1000); + } + }; + + private final SessionManager mSessionManager = new SessionManager("app"); + private Player mPlayer; + + private final MediaRouter.Callback mMediaRouterCB = new MediaRouter.Callback() { + // Return a custom callback that will simply log all of the route events + // for demonstration purposes. + @Override + public void onRouteAdded(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteAdded: route=" + route); + } + + @Override + public void onRouteChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteChanged: route=" + route); + } + + @Override + public void onRouteRemoved(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteRemoved: route=" + route); + } + + @Override + public void onRouteSelected(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteSelected: route=" + route); + + mPlayer = Player.create(MainActivity.this, route); + mPlayer.updatePresentation(); + mSessionManager.setPlayer(mPlayer); + mSessionManager.unsuspend(); + + registerRemoteControlClient(); + updateUi(); + } + + @Override + public void onRouteUnselected(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteUnselected: route=" + route); + unregisterRemoteControlClient(); + + PlaylistItem item = getCheckedPlaylistItem(); + if (item != null) { + long pos = item.getPosition() + + (mPaused ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp())); + mSessionManager.suspend(pos); + } + mPlayer.updatePresentation(); + mPlayer.release(); + } + + @Override + public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRouteVolumeChanged: route=" + route); + } + + @Override + public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { + Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route); + mPlayer.updatePresentation(); + } + + @Override + public void onProviderAdded(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderAdded: provider=" + provider); + } + + @Override + public void onProviderRemoved(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderRemoved: provider=" + provider); + } + + @Override + public void onProviderChanged(MediaRouter router, ProviderInfo provider) { + Log.d(TAG, "onRouteProviderChanged: provider=" + provider); + } + }; + + private final OnAudioFocusChangeListener mAfChangeListener = new OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) { + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + Log.d(TAG, "onAudioFocusChange: LOSS_TRANSIENT"); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_GAIN"); + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { + Log.d(TAG, "onAudioFocusChange: AUDIOFOCUS_LOSS"); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + // Be sure to call the super class. + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + mPlayer = (Player) savedInstanceState.getSerializable("mPlayer"); + } + + // Get the media router service. + mMediaRouter = MediaRouter.getInstance(this); + + // Create a route selector for the type of routes that we care about. + mSelector = + new MediaRouteSelector.Builder().addControlCategory(MediaControlIntent + .CATEGORY_LIVE_AUDIO).addControlCategory(MediaControlIntent + .CATEGORY_LIVE_VIDEO).addControlCategory(MediaControlIntent + .CATEGORY_REMOTE_PLAYBACK).addControlCategory(SampleMediaRouteProvider + .CATEGORY_SAMPLE_ROUTE).build(); + + // Add a fragment to take care of media route discovery. + // This fragment automatically adds or removes a callback whenever the activity + // is started or stopped. + FragmentManager fm = getSupportFragmentManager(); + DiscoveryFragment fragment = + (DiscoveryFragment) fm.findFragmentByTag(DISCOVERY_FRAGMENT_TAG); + if (fragment == null) { + fragment = new DiscoveryFragment(mMediaRouterCB); + fragment.setRouteSelector(mSelector); + fm.beginTransaction().add(fragment, DISCOVERY_FRAGMENT_TAG).commit(); + } else { + fragment.setCallback(mMediaRouterCB); + fragment.setRouteSelector(mSelector); + } + + // Populate an array adapter with streaming media items. + String[] mediaNames = getResources().getStringArray(R.array.media_names); + String[] mediaUris = getResources().getStringArray(R.array.media_uris); + mLibraryItems = new LibraryAdapter(); + for (int i = 0; i < mediaNames.length; i++) { + mLibraryItems.add(new MediaItem( + "[streaming] " + mediaNames[i], Uri.parse(mediaUris[i]), "video/mp4")); + } + + // Scan local external storage directory for media files. + File externalDir = Environment.getExternalStorageDirectory(); + if (externalDir != null) { + File list[] = externalDir.listFiles(); + if (list != null) { + for (int i = 0; i < list.length; i++) { + String filename = list[i].getName(); + if (filename.matches(".*\\.(m4v|mp4)")) { + mLibraryItems.add(new MediaItem( + "[local] " + filename, Uri.fromFile(list[i]), "video/mp4")); + } + } + } + } + + mPlayListItems = new PlaylistAdapter(); + + // Initialize the layout. + setContentView(R.layout.sample_media_router); + + TabHost tabHost = (TabHost) findViewById(R.id.tabHost); + tabHost.setup(); + String tabName = getResources().getString(R.string.library_tab_text); + TabSpec spec1 = tabHost.newTabSpec(tabName); + spec1.setContent(R.id.tab1); + spec1.setIndicator(tabName); + + tabName = getResources().getString(R.string.playlist_tab_text); + TabSpec spec2 = tabHost.newTabSpec(tabName); + spec2.setIndicator(tabName); + spec2.setContent(R.id.tab2); + + tabName = getResources().getString(R.string.statistics_tab_text); + TabSpec spec3 = tabHost.newTabSpec(tabName); + spec3.setIndicator(tabName); + spec3.setContent(R.id.tab3); + + tabHost.addTab(spec1); + tabHost.addTab(spec2); + tabHost.addTab(spec3); + tabHost.setOnTabChangedListener(new OnTabChangeListener() { + @Override + public void onTabChanged(String arg0) { + updateUi(); + } + }); + + mLibraryView = (ListView) findViewById(R.id.media); + mLibraryView.setAdapter(mLibraryItems); + mLibraryView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mLibraryView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + updateButtons(); + } + }); + + mPlayListView = (ListView) findViewById(R.id.playlist); + mPlayListView.setAdapter(mPlayListItems); + mPlayListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + mPlayListView.setOnItemClickListener(new OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + updateButtons(); + } + }); + + mInfoTextView = (TextView) findViewById(R.id.info); + + mPauseResumeButton = (ImageButton) findViewById(R.id.pause_resume_button); + mPauseResumeButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPaused = !mPaused; + if (mPaused) { + mSessionManager.pause(); + } else { + mSessionManager.resume(); + } + } + }); + + mStopButton = (ImageButton) findViewById(R.id.stop_button); + mStopButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPaused = false; + mSessionManager.stop(); + } + }); + + mSeekBar = (SeekBar) findViewById(R.id.seekbar); + mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + PlaylistItem item = getCheckedPlaylistItem(); + if (fromUser && item != null && item.getDuration() > 0) { + long pos = progress * item.getDuration() / 100; + mSessionManager.seek(item.getItemId(), pos); + item.setPosition(pos); + item.setTimestamp(SystemClock.elapsedRealtime()); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + mSeeking = true; + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + mSeeking = false; + updateUi(); + } + }); + + // Schedule Ui update + mHandler.postDelayed(mUpdateSeekRunnable, 1000); + + // Build the PendingIntent for the remote control client + mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + mEventReceiver = + new ComponentName(getPackageName(), SampleMediaButtonReceiver.class.getName()); + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(mEventReceiver); + mMediaPendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0); + + // Create and register the remote control client + registerRemoteControlClient(); + + // Set up playback manager and player + mPlayer = Player.create(MainActivity.this, mMediaRouter.getSelectedRoute()); + mSessionManager.setPlayer(mPlayer); + mSessionManager.setCallback(new SessionManager.Callback() { + @Override + public void onStatusChanged() { + updateUi(); + } + + @Override + public void onItemChanged(PlaylistItem item) { + } + }); + + updateUi(); + } + + private void registerRemoteControlClient() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + // Create the RCC and register with AudioManager and MediaRouter + mAudioManager.requestAudioFocus(mAfChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + mAudioManager.registerMediaButtonEventReceiver(mEventReceiver); + mRemoteControlClient = new RemoteControlClient(mMediaPendingIntent); + mAudioManager.registerRemoteControlClient(mRemoteControlClient); + mMediaRouter.addRemoteControlClient(mRemoteControlClient); + SampleMediaButtonReceiver.setActivity(MainActivity.this); + mRemoteControlClient.setTransportControlFlags(RemoteControlClient + .FLAG_KEY_MEDIA_PLAY_PAUSE); + mRemoteControlClient.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + } + + private void unregisterRemoteControlClient() { + // Unregister the RCC with AudioManager and MediaRouter + if (mRemoteControlClient != null) { + mRemoteControlClient.setTransportControlFlags(0); + mAudioManager.abandonAudioFocus(mAfChangeListener); + mAudioManager.unregisterMediaButtonEventReceiver(mEventReceiver); + mAudioManager.unregisterRemoteControlClient(mRemoteControlClient); + mMediaRouter.removeRemoteControlClient(mRemoteControlClient); + SampleMediaButtonReceiver.setActivity(null); + mRemoteControlClient = null; + } + } + + public boolean handleMediaKey(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: { + Log.d(TAG, "Received Play/Pause event from RemoteControlClient"); + mPaused = !mPaused; + if (mPaused) { + mSessionManager.pause(); + } else { + mSessionManager.resume(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PLAY: { + Log.d(TAG, "Received Play event from RemoteControlClient"); + if (mPaused) { + mPaused = false; + mSessionManager.resume(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PAUSE: { + Log.d(TAG, "Received Pause event from RemoteControlClient"); + if (!mPaused) { + mPaused = true; + mSessionManager.pause(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_STOP: { + Log.d(TAG, "Received Stop event from RemoteControlClient"); + mPaused = false; + mSessionManager.stop(); + return true; + } + default: + break; + } + } + return false; + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return handleMediaKey(event) || super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return handleMediaKey(event) || super.onKeyUp(keyCode, event); + } + + @Override + public void onStart() { + // Be sure to call the super class. + super.onStart(); + } + + @Override + public void onPause() { + // pause media player for local playback case only + if (!mPlayer.isRemotePlayback() && !mPaused) { + mNeedResume = true; + mSessionManager.pause(); + } + super.onPause(); + } + + @Override + public void onResume() { + // resume media player for local playback case only + if (!mPlayer.isRemotePlayback() && mNeedResume) { + mSessionManager.resume(); + mNeedResume = false; + } + super.onResume(); + } + + @Override + public void onDestroy() { + // Unregister the remote control client + unregisterRemoteControlClient(); + + mPaused = false; + mSessionManager.stop(); + mPlayer.release(); + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Be sure to call the super class. + super.onCreateOptionsMenu(menu); + + // Inflate the menu and configure the media router action provider. + getMenuInflater().inflate(R.menu.sample_media_router_menu, menu); + + MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item); + MediaRouteActionProvider mediaRouteActionProvider = + (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem); + mediaRouteActionProvider.setRouteSelector(mSelector); + + // Return true to show the menu. + return true; + } + + private void updateProgress() { + // Estimate content position from last status time and elapsed time. + // (Note this might be slightly out of sync with remote side, however + // it avoids frequent polling the MRP.) + int progress = 0; + PlaylistItem item = getCheckedPlaylistItem(); + if (item != null) { + int state = item.getState(); + long duration = item.getDuration(); + if (duration <= 0) { + if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING || + state == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mSessionManager.updateStatus(); + } + } else { + long position = item.getPosition(); + long timeDelta = + mPaused ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp()); + progress = (int) (100.0 * (position + timeDelta) / duration); + } + } + mSeekBar.setProgress(progress); + } + + private void updateUi() { + updatePlaylist(); + updateRouteDescription(); + updateButtons(); + } + + private void updatePlaylist() { + mPlayListItems.clear(); + for (PlaylistItem item : mSessionManager.getPlaylist()) { + mPlayListItems.add(item); + } + mPlayListView.invalidate(); + } + + + private void updateRouteDescription() { + RouteInfo route = mMediaRouter.getSelectedRoute(); + mInfoTextView.setText( + "Currently selected route:" + "\nName: " + route.getName() + "\nProvider: " + + route.getProvider().getPackageName() + "\nDescription: " + + route.getDescription() + "\nStatistics: " + + mSessionManager.getStatistics()); + } + + private void updateButtons() { + MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute(); + // show pause or resume icon depending on current state + mPauseResumeButton.setImageResource( + mPaused ? R.drawable.ic_action_play : R.drawable.ic_action_pause); + // disable pause/resume/stop if no session + mPauseResumeButton.setEnabled(mSessionManager.hasSession()); + mStopButton.setEnabled(mSessionManager.hasSession()); + // only enable seek bar when duration is known + PlaylistItem item = getCheckedPlaylistItem(); + mSeekBar.setEnabled(item != null && item.getDuration() > 0); + if (mRemoteControlClient != null) { + mRemoteControlClient.setPlaybackState(mPaused ? RemoteControlClient.PLAYSTATE_PAUSED : + RemoteControlClient.PLAYSTATE_PLAYING); + } + } + + private PlaylistItem getCheckedPlaylistItem() { + int count = mPlayListView.getCount(); + int index = mPlayListView.getCheckedItemPosition(); + if (count > 0) { + if (index < 0 || index >= count) { + index = 0; + mPlayListView.setItemChecked(0, true); + } + return mPlayListItems.getItem(index); + } + return null; + } + + public static final class DiscoveryFragment extends MediaRouteDiscoveryFragment { + private static final String TAG = "DiscoveryFragment"; + private Callback mCallback; + + public DiscoveryFragment() { + mCallback = null; + } + + public DiscoveryFragment(Callback cb) { + mCallback = cb; + } + + public void setCallback(Callback cb) { + mCallback = cb; + } + + @Override + public Callback onCreateCallback() { + return mCallback; + } + + @Override + public int onPrepareCallbackFlags() { + // Add the CALLBACK_FLAG_UNFILTERED_EVENTS flag to ensure that we will + // observe and log all route events including those that are for routes + // that do not match our selector. This is only for demonstration purposes + // and should not be needed by most applications. + return super.onPrepareCallbackFlags() | MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS; + } + } + + private static final class MediaItem { + public final String mName; + public final Uri mUri; + public final String mMime; + + public MediaItem(String name, Uri uri, String mime) { + mName = name; + mUri = uri; + mMime = mime; + } + + @Override + public String toString() { + return mName; + } + } + + private final class LibraryAdapter extends ArrayAdapter<MediaItem> { + public LibraryAdapter() { + super(MainActivity.this, R.layout.media_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View v; + if (convertView == null) { + v = getLayoutInflater().inflate(R.layout.media_item, null); + } else { + v = convertView; + } + + final MediaItem item = getItem(position); + + TextView tv = (TextView) v.findViewById(R.id.item_text); + tv.setText(item.mName); + + ImageButton b = (ImageButton) v.findViewById(R.id.item_action); + b.setImageResource(R.drawable.ic_suggestions_add); + b.setTag(item); + b.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (item != null) { + mSessionManager.add(item.mUri, item.mMime); + Toast.makeText(MainActivity.this, R.string.playlist_item_added_text, + Toast.LENGTH_SHORT).show(); + } + } + }); + + return v; + } + } + + private final class PlaylistAdapter extends ArrayAdapter<PlaylistItem> { + public PlaylistAdapter() { + super(MainActivity.this, R.layout.media_item); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final View v; + if (convertView == null) { + v = getLayoutInflater().inflate(R.layout.media_item, null); + } else { + v = convertView; + } + + final PlaylistItem item = getItem(position); + + TextView tv = (TextView) v.findViewById(R.id.item_text); + tv.setText(item.toString()); + + ImageButton b = (ImageButton) v.findViewById(R.id.item_action); + b.setImageResource(R.drawable.ic_suggestions_delete); + b.setTag(item); + b.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (item != null) { + mSessionManager.remove(item.getItemId()); + Toast.makeText(MainActivity.this, R.string.playlist_item_removed_text, + Toast.LENGTH_SHORT).show(); + } + } + }); + + return v; + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java new file mode 100644 index 000000000..58348308e --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/OverlayDisplayWindow.java @@ -0,0 +1,463 @@ +/* + * 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.example.android.mediarouter.player; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.TextureView.SurfaceTextureListener; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import com.example.android.mediarouter.R; + +/** + * Manages an overlay display window, used for simulating remote playback. + */ +public abstract class OverlayDisplayWindow { + private static final String TAG = "OverlayDisplayWindow"; + private static final boolean DEBUG = false; + + private static final float WINDOW_ALPHA = 0.8f; + private static final float INITIAL_SCALE = 0.5f; + private static final float MIN_SCALE = 0.3f; + private static final float MAX_SCALE = 1.0f; + + protected final Context mContext; + protected final String mName; + protected final int mWidth; + protected final int mHeight; + protected final int mGravity; + protected OverlayWindowListener mListener; + + protected OverlayDisplayWindow(Context context, String name, + int width, int height, int gravity) { + mContext = context; + mName = name; + mWidth = width; + mHeight = height; + mGravity = gravity; + } + + public static OverlayDisplayWindow create(Context context, String name, + int width, int height, int gravity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + return new JellybeanMr1Impl(context, name, width, height, gravity); + } else { + return new LegacyImpl(context, name, width, height, gravity); + } + } + + public void setOverlayWindowListener(OverlayWindowListener listener) { + mListener = listener; + } + + public Context getContext() { + return mContext; + } + + public abstract void show(); + + public abstract void dismiss(); + + public abstract void updateAspectRatio(int width, int height); + + // Watches for significant changes in the overlay display window lifecycle. + public interface OverlayWindowListener { + public void onWindowCreated(Surface surface); + public void onWindowCreated(SurfaceHolder surfaceHolder); + public void onWindowDestroyed(); + } + + /** + * Implementation for older versions. + */ + private static final class LegacyImpl extends OverlayDisplayWindow { + private final WindowManager mWindowManager; + + private boolean mWindowVisible; + private SurfaceView mSurfaceView; + + public LegacyImpl(Context context, String name, + int width, int height, int gravity) { + super(context, name, width, height, gravity); + + mWindowManager = (WindowManager)context.getSystemService( + Context.WINDOW_SERVICE); + } + + @Override + public void show() { + if (!mWindowVisible) { + mSurfaceView = new SurfaceView(mContext); + + Display display = mWindowManager.getDefaultDisplay(); + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + params.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + params.alpha = WINDOW_ALPHA; + params.gravity = Gravity.LEFT | Gravity.BOTTOM; + params.setTitle(mName); + + int width = (int)(display.getWidth() * INITIAL_SCALE); + int height = (int)(display.getHeight() * INITIAL_SCALE); + if (mWidth > mHeight) { + height = mHeight * width / mWidth; + } else { + width = mWidth * height / mHeight; + } + params.width = width; + params.height = height; + + mWindowManager.addView(mSurfaceView, params); + mWindowVisible = true; + + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + mListener.onWindowCreated(holder); + } + } + + @Override + public void dismiss() { + if (mWindowVisible) { + mListener.onWindowDestroyed(); + + mWindowManager.removeView(mSurfaceView); + mWindowVisible = false; + } + } + + @Override + public void updateAspectRatio(int width, int height) { + } + } + + /** + * Implementation for API version 17+. + */ + private static final class JellybeanMr1Impl extends OverlayDisplayWindow { + // When true, disables support for moving and resizing the overlay. + // The window is made non-touchable, which makes it possible to + // directly interact with the content underneath. + private static final boolean DISABLE_MOVE_AND_RESIZE = false; + + private final DisplayManager mDisplayManager; + private final WindowManager mWindowManager; + + private final Display mDefaultDisplay; + private final DisplayMetrics mDefaultDisplayMetrics = new DisplayMetrics(); + + private View mWindowContent; + private WindowManager.LayoutParams mWindowParams; + private TextureView mTextureView; + private TextView mNameTextView; + + private GestureDetector mGestureDetector; + private ScaleGestureDetector mScaleGestureDetector; + + private boolean mWindowVisible; + private int mWindowX; + private int mWindowY; + private float mWindowScale; + + private float mLiveTranslationX; + private float mLiveTranslationY; + private float mLiveScale = 1.0f; + + public JellybeanMr1Impl(Context context, String name, + int width, int height, int gravity) { + super(context, name, width, height, gravity); + + mDisplayManager = (DisplayManager)context.getSystemService( + Context.DISPLAY_SERVICE); + mWindowManager = (WindowManager)context.getSystemService( + Context.WINDOW_SERVICE); + + mDefaultDisplay = mWindowManager.getDefaultDisplay(); + updateDefaultDisplayInfo(); + + createWindow(); + } + + @Override + public void show() { + if (!mWindowVisible) { + mDisplayManager.registerDisplayListener(mDisplayListener, null); + if (!updateDefaultDisplayInfo()) { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + return; + } + + clearLiveState(); + updateWindowParams(); + mWindowManager.addView(mWindowContent, mWindowParams); + mWindowVisible = true; + } + } + + @Override + public void dismiss() { + if (mWindowVisible) { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + mWindowManager.removeView(mWindowContent); + mWindowVisible = false; + } + } + + @Override + public void updateAspectRatio(int width, int height) { + if (mWidth * height < mHeight * width) { + mTextureView.getLayoutParams().width = mWidth; + mTextureView.getLayoutParams().height = mWidth * height / width; + } else { + mTextureView.getLayoutParams().width = mHeight * width / height; + mTextureView.getLayoutParams().height = mHeight; + } + relayout(); + } + + private void relayout() { + if (mWindowVisible) { + updateWindowParams(); + mWindowManager.updateViewLayout(mWindowContent, mWindowParams); + } + } + + private boolean updateDefaultDisplayInfo() { + mDefaultDisplay.getMetrics(mDefaultDisplayMetrics); + return true; + } + + private void createWindow() { + LayoutInflater inflater = LayoutInflater.from(mContext); + + mWindowContent = inflater.inflate( + R.layout.overlay_display_window, null); + mWindowContent.setOnTouchListener(mOnTouchListener); + + mTextureView = (TextureView)mWindowContent.findViewById( + R.id.overlay_display_window_texture); + mTextureView.setPivotX(0); + mTextureView.setPivotY(0); + mTextureView.getLayoutParams().width = mWidth; + mTextureView.getLayoutParams().height = mHeight; + mTextureView.setOpaque(false); + mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); + + mNameTextView = (TextView)mWindowContent.findViewById( + R.id.overlay_display_window_title); + mNameTextView.setText(mName); + + mWindowParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + if (DISABLE_MOVE_AND_RESIZE) { + mWindowParams.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; + } + mWindowParams.alpha = WINDOW_ALPHA; + mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; + mWindowParams.setTitle(mName); + + mGestureDetector = new GestureDetector(mContext, mOnGestureListener); + mScaleGestureDetector = new ScaleGestureDetector(mContext, mOnScaleGestureListener); + + // Set the initial position and scale. + // The position and scale will be clamped when the display is first shown. + mWindowX = (mGravity & Gravity.LEFT) == Gravity.LEFT ? + 0 : mDefaultDisplayMetrics.widthPixels; + mWindowY = (mGravity & Gravity.TOP) == Gravity.TOP ? + 0 : mDefaultDisplayMetrics.heightPixels; + Log.d(TAG, mDefaultDisplayMetrics.toString()); + mWindowScale = INITIAL_SCALE; + + // calculate and save initial settings + updateWindowParams(); + saveWindowParams(); + } + + private void updateWindowParams() { + float scale = mWindowScale * mLiveScale; + scale = Math.min(scale, (float)mDefaultDisplayMetrics.widthPixels / mWidth); + scale = Math.min(scale, (float)mDefaultDisplayMetrics.heightPixels / mHeight); + scale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scale)); + + float offsetScale = (scale / mWindowScale - 1.0f) * 0.5f; + int width = (int)(mWidth * scale); + int height = (int)(mHeight * scale); + int x = (int)(mWindowX + mLiveTranslationX - width * offsetScale); + int y = (int)(mWindowY + mLiveTranslationY - height * offsetScale); + x = Math.max(0, Math.min(x, mDefaultDisplayMetrics.widthPixels - width)); + y = Math.max(0, Math.min(y, mDefaultDisplayMetrics.heightPixels - height)); + + if (DEBUG) { + Log.d(TAG, "updateWindowParams: scale=" + scale + + ", offsetScale=" + offsetScale + + ", x=" + x + ", y=" + y + + ", width=" + width + ", height=" + height); + } + + mTextureView.setScaleX(scale); + mTextureView.setScaleY(scale); + + mTextureView.setTranslationX( + (mWidth - mTextureView.getLayoutParams().width) * scale / 2); + mTextureView.setTranslationY( + (mHeight - mTextureView.getLayoutParams().height) * scale / 2); + + mWindowParams.x = x; + mWindowParams.y = y; + mWindowParams.width = width; + mWindowParams.height = height; + } + + private void saveWindowParams() { + mWindowX = mWindowParams.x; + mWindowY = mWindowParams.y; + mWindowScale = mTextureView.getScaleX(); + clearLiveState(); + } + + private void clearLiveState() { + mLiveTranslationX = 0f; + mLiveTranslationY = 0f; + mLiveScale = 1.0f; + } + + private final DisplayManager.DisplayListener mDisplayListener = + new DisplayManager.DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == mDefaultDisplay.getDisplayId()) { + if (updateDefaultDisplayInfo()) { + relayout(); + } else { + dismiss(); + } + } + } + + @Override + public void onDisplayRemoved(int displayId) { + if (displayId == mDefaultDisplay.getDisplayId()) { + dismiss(); + } + } + }; + + private final SurfaceTextureListener mSurfaceTextureListener = + new SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, + int width, int height) { + if (mListener != null) { + mListener.onWindowCreated(new Surface(surfaceTexture)); + } + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + if (mListener != null) { + mListener.onWindowDestroyed(); + } + return true; + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, + int width, int height) { + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + } + }; + + private final View.OnTouchListener mOnTouchListener = new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + // Work in screen coordinates. + final float oldX = event.getX(); + final float oldY = event.getY(); + event.setLocation(event.getRawX(), event.getRawY()); + + mGestureDetector.onTouchEvent(event); + mScaleGestureDetector.onTouchEvent(event); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + saveWindowParams(); + break; + } + + // Revert to window coordinates. + event.setLocation(oldX, oldY); + return true; + } + }; + + private final GestureDetector.OnGestureListener mOnGestureListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, + float distanceX, float distanceY) { + mLiveTranslationX -= distanceX; + mLiveTranslationY -= distanceY; + relayout(); + return true; + } + }; + + private final ScaleGestureDetector.OnScaleGestureListener mOnScaleGestureListener = + new ScaleGestureDetector.SimpleOnScaleGestureListener() { + @Override + public boolean onScale(ScaleGestureDetector detector) { + mLiveScale *= detector.getScaleFactor(); + relayout(); + return true; + } + }; + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java new file mode 100644 index 000000000..f842cf66f --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/Player.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.Context; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouter.RouteInfo; + +/** + * Abstraction of common playback operations of media items, such as play, + * seek, etc. Used by PlaybackManager as a backend to handle actual playback + * of media items. + */ +public abstract class Player { + protected Callback mCallback; + + public abstract boolean isRemotePlayback(); + public abstract boolean isQueuingSupported(); + + public abstract void connect(RouteInfo route); + public abstract void release(); + + // basic operations that are always supported + public abstract void play(final PlaylistItem item); + public abstract void seek(final PlaylistItem item); + public abstract void getStatus(final PlaylistItem item, final boolean update); + public abstract void pause(); + public abstract void resume(); + public abstract void stop(); + + // advanced queuing (enqueue & remove) are only supported + // if isQueuingSupported() returns true + public abstract void enqueue(final PlaylistItem item); + public abstract PlaylistItem remove(String iid); + + // route statistics + public void updateStatistics() {} + public String getStatistics() { return ""; } + + // presentation display + public void updatePresentation() {} + + public void setCallback(Callback callback) { + mCallback = callback; + } + + public static Player create(Context context, RouteInfo route) { + Player player; + if (route != null && route.supportsControlCategory( + MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { + player = new RemotePlayer(context); + } else if (route != null) { + player = new LocalPlayer.SurfaceViewPlayer(context); + } else { + player = new LocalPlayer.OverlayPlayer(context); + } + player.connect(route); + return player; + } + + public interface Callback { + void onError(); + void onCompletion(); + void onPlaylistChanged(); + void onPlaylistReady(); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java new file mode 100644 index 000000000..a5605384a --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/PlaylistItem.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.net.Uri; +import android.os.SystemClock; +import android.support.v7.media.MediaItemStatus; + +/** + * PlaylistItem helps keep track of the current status of an media item. + */ +public final class PlaylistItem { + // immutables + private final String mSessionId; + private final String mItemId; + private final Uri mUri; + private final String mMime; + private final PendingIntent mUpdateReceiver; + // changeable states + private int mPlaybackState = MediaItemStatus.PLAYBACK_STATE_PENDING; + private long mContentPosition; + private long mContentDuration; + private long mTimestamp; + private String mRemoteItemId; + + public PlaylistItem(String qid, String iid, Uri uri, String mime, PendingIntent pi) { + mSessionId = qid; + mItemId = iid; + mUri = uri; + mMime = mime; + mUpdateReceiver = pi; + setTimestamp(SystemClock.elapsedRealtime()); + } + + public void setRemoteItemId(String riid) { + mRemoteItemId = riid; + } + + public void setState(int state) { + mPlaybackState = state; + } + + public void setPosition(long pos) { + mContentPosition = pos; + } + + public void setTimestamp(long ts) { + mTimestamp = ts; + } + + public void setDuration(long duration) { + mContentDuration = duration; + } + + public String getSessionId() { + return mSessionId; + } + + public String getItemId() { + return mItemId; + } + + public String getRemoteItemId() { + return mRemoteItemId; + } + + public Uri getUri() { + return mUri; + } + + public PendingIntent getUpdateReceiver() { + return mUpdateReceiver; + } + + public int getState() { + return mPlaybackState; + } + + public long getPosition() { + return mContentPosition; + } + + public long getDuration() { + return mContentDuration; + } + + public long getTimestamp() { + return mTimestamp; + } + + public MediaItemStatus getStatus() { + return new MediaItemStatus.Builder(mPlaybackState) + .setContentPosition(mContentPosition) + .setContentDuration(mContentDuration) + .setTimestamp(mTimestamp) + .build(); + } + + @Override + public String toString() { + String state[] = { + "PENDING", + "PLAYING", + "PAUSED", + "BUFFERING", + "FINISHED", + "CANCELED", + "INVALIDATED", + "ERROR" + }; + return "[" + mSessionId + "|" + mItemId + "|" + + (mRemoteItemId != null ? mRemoteItemId : "-") + "|" + + state[mPlaybackState] + "] " + mUri.toString(); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java new file mode 100644 index 000000000..672671887 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/RemotePlayer.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaRouter.ControlRequestCallback; +import android.support.v7.media.MediaRouter.RouteInfo; +import android.support.v7.media.MediaSessionStatus; +import android.support.v7.media.RemotePlaybackClient; +import android.support.v7.media.RemotePlaybackClient.ItemActionCallback; +import android.support.v7.media.RemotePlaybackClient.SessionActionCallback; +import android.support.v7.media.RemotePlaybackClient.StatusCallback; +import android.util.Log; + +import com.example.android.mediarouter.player.Player; +import com.example.android.mediarouter.player.PlaylistItem; +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +import java.util.ArrayList; +import java.util.List; + +/** + * Handles playback of media items using a remote route. + * + * This class is used as a backend by PlaybackManager to feed media items to + * the remote route. When the remote route doesn't support queuing, media items + * are fed one-at-a-time; otherwise media items are enqueued to the remote side. + */ +public class RemotePlayer extends Player { + private static final String TAG = "RemotePlayer"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private Context mContext; + private RouteInfo mRoute; + private boolean mEnqueuePending; + private String mStatsInfo = ""; + private List<PlaylistItem> mTempQueue = new ArrayList<PlaylistItem>(); + + private RemotePlaybackClient mClient; + private StatusCallback mStatusCallback = new StatusCallback() { + @Override + public void onItemStatusChanged(Bundle data, + String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("onItemStatusChanged", sessionId, sessionStatus, itemId, itemStatus); + if (mCallback != null) { + if (itemStatus.getPlaybackState() == + MediaItemStatus.PLAYBACK_STATE_FINISHED) { + mCallback.onCompletion(); + } else if (itemStatus.getPlaybackState() == + MediaItemStatus.PLAYBACK_STATE_ERROR) { + mCallback.onError(); + } + } + } + + @Override + public void onSessionStatusChanged(Bundle data, + String sessionId, MediaSessionStatus sessionStatus) { + logStatus("onSessionStatusChanged", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onSessionChanged(String sessionId) { + if (DEBUG) { + Log.d(TAG, "onSessionChanged: sessionId=" + sessionId); + } + } + }; + + public RemotePlayer(Context context) { + mContext = context; + } + + @Override + public boolean isRemotePlayback() { + return true; + } + + @Override + public boolean isQueuingSupported() { + return mClient.isQueuingSupported(); + } + + @Override + public void connect(RouteInfo route) { + mRoute = route; + mClient = new RemotePlaybackClient(mContext, route); + mClient.setStatusCallback(mStatusCallback); + + if (DEBUG) { + Log.d(TAG, "connected to: " + route + + ", isRemotePlaybackSupported: " + mClient.isRemotePlaybackSupported() + + ", isQueuingSupported: "+ mClient.isQueuingSupported()); + } + } + + @Override + public void release() { + mClient.release(); + + if (DEBUG) { + Log.d(TAG, "released."); + } + } + + // basic playback operations that are always supported + @Override + public void play(final PlaylistItem item) { + if (DEBUG) { + Log.d(TAG, "play: item=" + item); + } + mClient.play(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("play: succeeded", sessionId, sessionStatus, itemId, itemStatus); + item.setRemoteItemId(itemId); + if (item.getPosition() > 0) { + seekInternal(item); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + pause(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("play: failed", error, code); + } + }); + } + + @Override + public void seek(final PlaylistItem item) { + seekInternal(item); + } + + @Override + public void getStatus(final PlaylistItem item, final boolean update) { + if (!mClient.hasSession() || item.getRemoteItemId() == null) { + // if session is not valid or item id not assigend yet. + // just return, it's not fatal + return; + } + + if (DEBUG) { + Log.d(TAG, "getStatus: item=" + item + ", update=" + update); + } + mClient.getStatus(item.getRemoteItemId(), null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("getStatus: succeeded", sessionId, sessionStatus, itemId, itemStatus); + int state = itemStatus.getPlaybackState(); + if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING + || state == MediaItemStatus.PLAYBACK_STATE_PAUSED + || state == MediaItemStatus.PLAYBACK_STATE_PENDING) { + item.setState(state); + item.setPosition(itemStatus.getContentPosition()); + item.setDuration(itemStatus.getContentDuration()); + item.setTimestamp(itemStatus.getTimestamp()); + } + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("getStatus: failed", error, code); + if (update && mCallback != null) { + mCallback.onPlaylistReady(); + } + } + }); + } + + @Override + public void pause() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "pause"); + } + mClient.pause(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("pause: succeeded", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("pause: failed", error, code); + } + }); + } + + @Override + public void resume() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "resume"); + } + mClient.resume(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("resume: succeeded", sessionId, sessionStatus, null, null); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("resume: failed", error, code); + } + }); + } + + @Override + public void stop() { + if (!mClient.hasSession()) { + // ignore if no session + return; + } + if (DEBUG) { + Log.d(TAG, "stop"); + } + mClient.stop(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("stop: succeeded", sessionId, sessionStatus, null, null); + if (mClient.isSessionManagementSupported()) { + endSession(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("stop: failed", error, code); + } + }); + } + + // enqueue & remove are only supported if isQueuingSupported() returns true + @Override + public void enqueue(final PlaylistItem item) { + throwIfQueuingUnsupported(); + + if (!mClient.hasSession() && !mEnqueuePending) { + mEnqueuePending = true; + if (mClient.isSessionManagementSupported()) { + startSession(item); + } else { + enqueueInternal(item); + } + } else if (mEnqueuePending){ + mTempQueue.add(item); + } else { + enqueueInternal(item); + } + } + + @Override + public PlaylistItem remove(String itemId) { + throwIfNoSession(); + throwIfQueuingUnsupported(); + + if (DEBUG) { + Log.d(TAG, "remove: itemId=" + itemId); + } + mClient.remove(itemId, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("remove: succeeded", sessionId, sessionStatus, itemId, itemStatus); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("remove: failed", error, code); + } + }); + + return null; + } + + @Override + public void updateStatistics() { + // clear stats info first + mStatsInfo = ""; + + Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_STATISTICS); + intent.addCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE); + + if (mRoute != null && mRoute.supportsControlRequest(intent)) { + ControlRequestCallback callback = new ControlRequestCallback() { + @Override + public void onResult(Bundle data) { + if (DEBUG) { + Log.d(TAG, "getStatistics: succeeded: data=" + data); + } + if (data != null) { + int playbackCount = data.getInt( + SampleMediaRouteProvider.DATA_PLAYBACK_COUNT, -1); + mStatsInfo = "Total playback count: " + playbackCount; + } + } + + @Override + public void onError(String error, Bundle data) { + Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data); + } + }; + + mRoute.sendControlRequest(intent, callback); + } + } + + @Override + public String getStatistics() { + return mStatsInfo; + } + + private void enqueueInternal(final PlaylistItem item) { + throwIfQueuingUnsupported(); + + if (DEBUG) { + Log.d(TAG, "enqueue: item=" + item); + } + mClient.enqueue(item.getUri(), "video/mp4", null, 0, null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("enqueue: succeeded", sessionId, sessionStatus, itemId, itemStatus); + item.setRemoteItemId(itemId); + if (item.getPosition() > 0) { + seekInternal(item); + } + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + pause(); + } + if (mEnqueuePending) { + mEnqueuePending = false; + for (PlaylistItem item : mTempQueue) { + enqueueInternal(item); + } + mTempQueue.clear(); + } + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("enqueue: failed", error, code); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + }); + } + + private void seekInternal(final PlaylistItem item) { + throwIfNoSession(); + + if (DEBUG) { + Log.d(TAG, "seek: item=" + item); + } + mClient.seek(item.getRemoteItemId(), item.getPosition(), null, new ItemActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + logStatus("seek: succeeded", sessionId, sessionStatus, itemId, itemStatus); + if (mCallback != null) { + mCallback.onPlaylistChanged(); + } + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("seek: failed", error, code); + } + }); + } + + private void startSession(final PlaylistItem item) { + mClient.startSession(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("startSession: succeeded", sessionId, sessionStatus, null, null); + enqueueInternal(item); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("startSession: failed", error, code); + } + }); + } + + private void endSession() { + mClient.endSession(null, new SessionActionCallback() { + @Override + public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) { + logStatus("endSession: succeeded", sessionId, sessionStatus, null, null); + } + + @Override + public void onError(String error, int code, Bundle data) { + logError("endSession: failed", error, code); + } + }); + } + + private void logStatus(String message, + String sessionId, MediaSessionStatus sessionStatus, + String itemId, MediaItemStatus itemStatus) { + if (DEBUG) { + String result = ""; + if (sessionId != null && sessionStatus != null) { + result += "sessionId=" + sessionId + ", sessionStatus=" + sessionStatus; + } + if (itemId != null & itemStatus != null) { + result += (result.isEmpty() ? "" : ", ") + + "itemId=" + itemId + ", itemStatus=" + itemStatus; + } + Log.d(TAG, message + ": " + result); + } + } + + private void logError(String message, String error, int code) { + Log.d(TAG, message + ": error=" + error + ", code=" + code); + } + + private void throwIfNoSession() { + if (!mClient.hasSession()) { + throw new IllegalStateException("Session is invalid"); + } + } + + private void throwIfQueuingUnsupported() { + if (!isQueuingSupported()) { + throw new UnsupportedOperationException("Queuing is unsupported"); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java new file mode 100644 index 000000000..855bc1eb7 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SampleMediaButtonReceiver.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.view.KeyEvent; + +/** + * Broadcast receiver for handling ACTION_MEDIA_BUTTON. + * + * This is needed to create the RemoteControlClient for controlling + * remote route volume in lock screen. It routes media key events back + * to main app activity MainActivity. + */ +public class SampleMediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "SampleMediaButtonReceiver"; + private static MainActivity mActivity; + + public static void setActivity(MainActivity activity) { + mActivity = activity; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (mActivity != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { + mActivity.handleMediaKey( + (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)); + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java new file mode 100644 index 000000000..b6c5a46c2 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/player/SessionManager.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.player; + +import android.app.PendingIntent; +import android.net.Uri; +import android.support.v7.media.MediaItemStatus; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +import com.example.android.mediarouter.player.Player.Callback; + +import java.util.ArrayList; +import java.util.List; + +/** + * SessionManager manages a media session as a queue. It supports common + * queuing behaviors such as enqueue/remove of media items, pause/resume/stop, + * etc. + * + * Actual playback of a single media item is abstracted into a Player interface, + * and is handled outside this class. + */ +public class SessionManager implements Callback { + private static final String TAG = "SessionManager"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private String mName; + private int mSessionId; + private int mItemId; + private boolean mPaused; + private boolean mSessionValid; + private Player mPlayer; + private Callback mCallback; + private List<PlaylistItem> mPlaylist = new ArrayList<PlaylistItem>(); + + public SessionManager(String name) { + mName = name; + } + + public boolean hasSession() { + return mSessionValid; + } + + public String getSessionId() { + return mSessionValid ? Integer.toString(mSessionId) : null; + } + + public PlaylistItem getCurrentItem() { + return mPlaylist.isEmpty() ? null : mPlaylist.get(0); + } + + // Get the cached statistic info from the player (will not update it) + public String getStatistics() { + checkPlayer(); + return mPlayer.getStatistics(); + } + + // Returns the cached playlist (note this is not responsible for updating it) + public List<PlaylistItem> getPlaylist() { + return mPlaylist; + } + + // Updates the playlist asynchronously, calls onPlaylistReady() when finished. + public void updateStatus() { + if (DEBUG) { + log("updateStatus"); + } + checkPlayer(); + // update the statistics first, so that the stats string is valid when + // onPlaylistReady() gets called in the end + mPlayer.updateStatistics(); + + if (mPlaylist.isEmpty()) { + // If queue is empty, don't forget to call onPlaylistReady()! + onPlaylistReady(); + } else if (mPlayer.isQueuingSupported()) { + // If player supports queuing, get status of each item. Player is + // responsible to call onPlaylistReady() after last getStatus(). + // (update=1 requires player to callback onPlaylistReady()) + for (int i = 0; i < mPlaylist.size(); i++) { + PlaylistItem item = mPlaylist.get(i); + mPlayer.getStatus(item, (i == mPlaylist.size() - 1) /* update */); + } + } else { + // Otherwise, only need to get status for current item. Player is + // responsible to call onPlaylistReady() when finished. + mPlayer.getStatus(getCurrentItem(), true /* update */); + } + } + + public PlaylistItem add(Uri uri, String mime) { + return add(uri, mime, null); + } + + public PlaylistItem add(Uri uri, String mime, PendingIntent receiver) { + if (DEBUG) { + log("add: uri=" + uri + ", receiver=" + receiver); + } + // create new session if needed + startSession(); + checkPlayerAndSession(); + + // append new item with initial status PLAYBACK_STATE_PENDING + PlaylistItem item = new PlaylistItem( + Integer.toString(mSessionId), Integer.toString(mItemId), uri, mime, receiver); + mPlaylist.add(item); + mItemId++; + + // if player supports queuing, enqueue the item now + if (mPlayer.isQueuingSupported()) { + mPlayer.enqueue(item); + } + updatePlaybackState(); + return item; + } + + public PlaylistItem remove(String iid) { + if (DEBUG) { + log("remove: iid=" + iid); + } + checkPlayerAndSession(); + return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED); + } + + public PlaylistItem seek(String iid, long pos) { + if (DEBUG) { + log("seek: iid=" + iid +", pos=" + pos); + } + checkPlayerAndSession(); + // seeking on pending items are not yet supported + checkItemCurrent(iid); + + PlaylistItem item = getCurrentItem(); + if (pos != item.getPosition()) { + item.setPosition(pos); + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.seek(item); + } + } + return item; + } + + public PlaylistItem getStatus(String iid) { + checkPlayerAndSession(); + + // This should only be called for local player. Remote player is + // asynchronous, need to use updateStatus() instead. + if (mPlayer.isRemotePlayback()) { + throw new IllegalStateException( + "getStatus should not be called on remote player!"); + } + + for (PlaylistItem item : mPlaylist) { + if (item.getItemId().equals(iid)) { + if (item == getCurrentItem()) { + mPlayer.getStatus(item, false); + } + return item; + } + } + return null; + } + + public void pause() { + if (DEBUG) { + log("pause"); + } + mPaused = true; + updatePlaybackState(); + } + + public void resume() { + if (DEBUG) { + log("resume"); + } + mPaused = false; + updatePlaybackState(); + } + + public void stop() { + if (DEBUG) { + log("stop"); + } + mPlayer.stop(); + mPlaylist.clear(); + mPaused = false; + updateStatus(); + } + + public String startSession() { + if (!mSessionValid) { + mSessionId++; + mItemId = 0; + mPaused = false; + mSessionValid = true; + return Integer.toString(mSessionId); + } + return null; + } + + public boolean endSession() { + if (mSessionValid) { + mSessionValid = false; + return true; + } + return false; + } + + public MediaSessionStatus getSessionStatus(String sid) { + int sessionState = (sid != null && sid.equals(mSessionId)) ? + MediaSessionStatus.SESSION_STATE_ACTIVE : + MediaSessionStatus.SESSION_STATE_INVALIDATED; + + return new MediaSessionStatus.Builder(sessionState) + .setQueuePaused(mPaused) + .build(); + } + + // Suspend the playback manager. Put the current item back into PENDING + // state, and remember the current playback position. Called when switching + // to a different player (route). + public void suspend(long pos) { + for (PlaylistItem item : mPlaylist) { + item.setRemoteItemId(null); + item.setDuration(0); + } + PlaylistItem item = getCurrentItem(); + if (DEBUG) { + log("suspend: item=" + item + ", pos=" + pos); + } + if (item != null) { + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + item.setState(MediaItemStatus.PLAYBACK_STATE_PENDING); + item.setPosition(pos); + } + } + } + + // Unsuspend the playback manager. Restart playback on new player (route). + // This will resume playback of current item. Furthermore, if the new player + // supports queuing, playlist will be re-established on the remote player. + public void unsuspend() { + if (DEBUG) { + log("unsuspend"); + } + if (mPlayer.isQueuingSupported()) { + for (PlaylistItem item : mPlaylist) { + mPlayer.enqueue(item); + } + } + updatePlaybackState(); + } + + // Player.Callback + @Override + public void onError() { + finishItem(true); + } + + @Override + public void onCompletion() { + finishItem(false); + } + + @Override + public void onPlaylistChanged() { + // Playlist has changed, update the cached playlist + updateStatus(); + } + + @Override + public void onPlaylistReady() { + // Notify activity to update Ui + if (mCallback != null) { + mCallback.onStatusChanged(); + } + } + + private void log(String message) { + Log.d(TAG, mName + ": " + message); + } + + private void checkPlayer() { + if (mPlayer == null) { + throw new IllegalStateException("Player not set!"); + } + } + + private void checkSession() { + if (!mSessionValid) { + throw new IllegalStateException("Session not set!"); + } + } + + private void checkPlayerAndSession() { + checkPlayer(); + checkSession(); + } + + private void checkItemCurrent(String iid) { + PlaylistItem item = getCurrentItem(); + if (item == null || !item.getItemId().equals(iid)) { + throw new IllegalArgumentException("Item is not current!"); + } + } + + private void updatePlaybackState() { + PlaylistItem item = getCurrentItem(); + if (item != null) { + if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) { + item.setState(mPaused ? MediaItemStatus.PLAYBACK_STATE_PAUSED + : MediaItemStatus.PLAYBACK_STATE_PLAYING); + if (!mPlayer.isQueuingSupported()) { + mPlayer.play(item); + } + } else if (mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) { + mPlayer.pause(); + item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED); + } else if (!mPaused && item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) { + mPlayer.resume(); + item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING); + } + // notify client that item playback status has changed + if (mCallback != null) { + mCallback.onItemChanged(item); + } + } + updateStatus(); + } + + private PlaylistItem removeItem(String iid, int state) { + checkPlayerAndSession(); + List<PlaylistItem> queue = + new ArrayList<PlaylistItem>(mPlaylist.size()); + PlaylistItem found = null; + for (PlaylistItem item : mPlaylist) { + if (iid.equals(item.getItemId())) { + if (mPlayer.isQueuingSupported()) { + mPlayer.remove(item.getRemoteItemId()); + } else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING + || item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED){ + mPlayer.stop(); + } + item.setState(state); + found = item; + // notify client that item is now removed + if (mCallback != null) { + mCallback.onItemChanged(found); + } + } else { + queue.add(item); + } + } + if (found != null) { + mPlaylist = queue; + updatePlaybackState(); + } else { + log("item not found"); + } + return found; + } + + private void finishItem(boolean error) { + PlaylistItem item = getCurrentItem(); + if (item != null) { + removeItem(item.getItemId(), error ? + MediaItemStatus.PLAYBACK_STATE_ERROR : + MediaItemStatus.PLAYBACK_STATE_FINISHED); + updateStatus(); + } + } + + // set the Player that this playback manager will interact with + public void setPlayer(Player player) { + mPlayer = player; + checkPlayer(); + mPlayer.setCallback(this); + } + + // provide a callback interface to tell the UI when significant state changes occur + public void setCallback(Callback callback) { + mCallback = callback; + } + + @Override + public String toString() { + String result = "Media Queue: "; + if (!mPlaylist.isEmpty()) { + for (PlaylistItem item : mPlaylist) { + result += "\n" + item.toString(); + } + } else { + result += "<empty>"; + } + return result; + } + + public interface Callback { + void onStatusChanged(); + void onItemChanged(PlaylistItem item); + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java new file mode 100644 index 000000000..739e3ba73 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProvider.java @@ -0,0 +1,602 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.provider; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentFilter.MalformedMimeTypeException; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.MediaRouter; +import android.net.Uri; +import android.os.Bundle; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter.ControlRequestCallback; +import android.support.v7.media.MediaSessionStatus; +import android.util.Log; + +import com.example.android.mediarouter.player.Player; +import com.example.android.mediarouter.player.PlaylistItem; +import com.example.android.mediarouter.R; +import com.example.android.mediarouter.player.SessionManager; + +import java.util.ArrayList; + +/** + * Demonstrates how to create a custom media route provider. + * + * @see SampleMediaRouteProviderService + */ +public final class SampleMediaRouteProvider extends MediaRouteProvider { + private static final String TAG = "SampleMediaRouteProvider"; + + private static final String FIXED_VOLUME_ROUTE_ID = "fixed"; + private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic"; + private static final String VARIABLE_VOLUME_QUEUING_ROUTE_ID = "variable_queuing"; + private static final String VARIABLE_VOLUME_SESSION_ROUTE_ID = "variable_session"; + private static final int VOLUME_MAX = 10; + + /** + * A custom media control intent category for special requests that are + * supported by this provider's routes. + */ + public static final String CATEGORY_SAMPLE_ROUTE = + "com.example.android.mediarouteprovider.CATEGORY_SAMPLE_ROUTE"; + + /** + * A custom media control intent action for special requests that are + * supported by this provider's routes. + * <p> + * This particular request is designed to return a bundle of not very + * interesting statistics for demonstration purposes. + * </p> + * + * @see #DATA_PLAYBACK_COUNT + */ + public static final String ACTION_GET_STATISTICS = + "com.example.android.mediarouteprovider.ACTION_GET_STATISTICS"; + + /** + * {@link #ACTION_GET_STATISTICS} result data: Number of times the + * playback action was invoked. + */ + public static final String DATA_PLAYBACK_COUNT = + "com.example.android.mediarouteprovider.EXTRA_PLAYBACK_COUNT"; + + private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC; + private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING; + private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION; + + static { + IntentFilter f1 = new IntentFilter(); + f1.addCategory(CATEGORY_SAMPLE_ROUTE); + f1.addAction(ACTION_GET_STATISTICS); + + IntentFilter f2 = new IntentFilter(); + f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f2.addAction(MediaControlIntent.ACTION_PLAY); + f2.addDataScheme("http"); + f2.addDataScheme("https"); + f2.addDataScheme("rtsp"); + f2.addDataScheme("file"); + addDataTypeUnchecked(f2, "video/*"); + + IntentFilter f3 = new IntentFilter(); + f3.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f3.addAction(MediaControlIntent.ACTION_SEEK); + f3.addAction(MediaControlIntent.ACTION_GET_STATUS); + f3.addAction(MediaControlIntent.ACTION_PAUSE); + f3.addAction(MediaControlIntent.ACTION_RESUME); + f3.addAction(MediaControlIntent.ACTION_STOP); + + IntentFilter f4 = new IntentFilter(); + f4.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f4.addAction(MediaControlIntent.ACTION_ENQUEUE); + f4.addDataScheme("http"); + f4.addDataScheme("https"); + f4.addDataScheme("rtsp"); + f4.addDataScheme("file"); + addDataTypeUnchecked(f4, "video/*"); + + IntentFilter f5 = new IntentFilter(); + f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f5.addAction(MediaControlIntent.ACTION_REMOVE); + + IntentFilter f6 = new IntentFilter(); + f6.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); + f6.addAction(MediaControlIntent.ACTION_START_SESSION); + f6.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS); + f6.addAction(MediaControlIntent.ACTION_END_SESSION); + + CONTROL_FILTERS_BASIC = new ArrayList<IntentFilter>(); + CONTROL_FILTERS_BASIC.add(f1); + CONTROL_FILTERS_BASIC.add(f2); + CONTROL_FILTERS_BASIC.add(f3); + + CONTROL_FILTERS_QUEUING = + new ArrayList<IntentFilter>(CONTROL_FILTERS_BASIC); + CONTROL_FILTERS_QUEUING.add(f4); + CONTROL_FILTERS_QUEUING.add(f5); + + CONTROL_FILTERS_SESSION = + new ArrayList<IntentFilter>(CONTROL_FILTERS_QUEUING); + CONTROL_FILTERS_SESSION.add(f6); + } + + private static void addDataTypeUnchecked(IntentFilter filter, String type) { + try { + filter.addDataType(type); + } catch (MalformedMimeTypeException ex) { + throw new RuntimeException(ex); + } + } + + private int mVolume = 5; + private int mEnqueueCount; + + public SampleMediaRouteProvider(Context context) { + super(context); + + publishRoutes(); + } + + @Override + public RouteController onCreateRouteController(String routeId) { + return new SampleRouteController(routeId); + } + + private void publishRoutes() { + Resources r = getContext().getResources(); + + MediaRouteDescriptor routeDescriptor1 = new MediaRouteDescriptor.Builder( + FIXED_VOLUME_ROUTE_ID, + r.getString(R.string.fixed_volume_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_BASIC) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED) + .setVolume(VOLUME_MAX) + .build(); + + MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_BASIC_ROUTE_ID, + r.getString(R.string.variable_volume_basic_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_BASIC) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteDescriptor routeDescriptor3 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_QUEUING_ROUTE_ID, + r.getString(R.string.variable_volume_queuing_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_QUEUING) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteDescriptor routeDescriptor4 = new MediaRouteDescriptor.Builder( + VARIABLE_VOLUME_SESSION_ROUTE_ID, + r.getString(R.string.variable_volume_session_route_name)) + .setDescription(r.getString(R.string.sample_route_description)) + .addControlFilters(CONTROL_FILTERS_SESSION) + .setPlaybackStream(AudioManager.STREAM_MUSIC) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(VOLUME_MAX) + .setVolume(mVolume) + .build(); + + MediaRouteProviderDescriptor providerDescriptor = + new MediaRouteProviderDescriptor.Builder() + .addRoute(routeDescriptor1) + .addRoute(routeDescriptor2) + .addRoute(routeDescriptor3) + .addRoute(routeDescriptor4) + .build(); + setDescriptor(providerDescriptor); + } + + private final class SampleRouteController extends MediaRouteProvider.RouteController { + private final String mRouteId; + private final SessionManager mSessionManager = new SessionManager("mrp"); + private final Player mPlayer; + private PendingIntent mSessionReceiver; + + public SampleRouteController(String routeId) { + mRouteId = routeId; + mPlayer = Player.create(getContext(), null); + mSessionManager.setPlayer(mPlayer); + mSessionManager.setCallback(new SessionManager.Callback() { + @Override + public void onStatusChanged() { + } + + @Override + public void onItemChanged(PlaylistItem item) { + handleStatusChange(item); + } + }); + Log.d(TAG, mRouteId + ": Controller created"); + } + + @Override + public void onRelease() { + Log.d(TAG, mRouteId + ": Controller released"); + mPlayer.release(); + } + + @Override + public void onSelect() { + Log.d(TAG, mRouteId + ": Selected"); + mPlayer.connect(null); + } + + @Override + public void onUnselect() { + Log.d(TAG, mRouteId + ": Unselected"); + mPlayer.release(); + } + + @Override + public void onSetVolume(int volume) { + Log.d(TAG, mRouteId + ": Set volume to " + volume); + if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) { + setVolumeInternal(volume); + } + } + + @Override + public void onUpdateVolume(int delta) { + Log.d(TAG, mRouteId + ": Update volume by " + delta); + if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) { + setVolumeInternal(mVolume + delta); + } + } + + @Override + public boolean onControlRequest(Intent intent, ControlRequestCallback callback) { + Log.d(TAG, mRouteId + ": Received control request " + intent); + String action = intent.getAction(); + if (intent.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { + boolean success = false; + if (action.equals(MediaControlIntent.ACTION_PLAY)) { + success = handlePlay(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) { + success = handleEnqueue(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_REMOVE)) { + success = handleRemove(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_SEEK)) { + success = handleSeek(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_STATUS)) { + success = handleGetStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_PAUSE)) { + success = handlePause(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_RESUME)) { + success = handleResume(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_STOP)) { + success = handleStop(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_START_SESSION)) { + success = handleStartSession(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_GET_SESSION_STATUS)) { + success = handleGetSessionStatus(intent, callback); + } else if (action.equals(MediaControlIntent.ACTION_END_SESSION)) { + success = handleEndSession(intent, callback); + } + Log.d(TAG, mSessionManager.toString()); + return success; + } + + if (action.equals(ACTION_GET_STATISTICS) + && intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) { + Bundle data = new Bundle(); + data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount); + if (callback != null) { + callback.onResult(data); + } + return true; + } + return false; + } + + private void setVolumeInternal(int volume) { + if (volume >= 0 && volume <= VOLUME_MAX) { + mVolume = volume; + Log.d(TAG, mRouteId + ": New volume is " + mVolume); + AudioManager audioManager = + (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, 0); + publishRoutes(); + } + } + + private boolean handlePlay(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid != null && !sid.equals(mSessionManager.getSessionId())) { + Log.d(TAG, "handlePlay fails because of bad sid="+sid); + return false; + } + if (mSessionManager.hasSession()) { + mSessionManager.stop(); + } + return handleEnqueue(intent, callback); + } + + private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid != null && !sid.equals(mSessionManager.getSessionId())) { + Log.d(TAG, "handleEnqueue fails because of bad sid="+sid); + return false; + } + + Uri uri = intent.getData(); + if (uri == null) { + Log.d(TAG, "handleEnqueue fails because of bad uri="+uri); + return false; + } + + boolean enqueue = intent.getAction().equals(MediaControlIntent.ACTION_ENQUEUE); + String mime = intent.getType(); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + Bundle metadata = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_METADATA); + Bundle headers = intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_HTTP_HEADERS); + PendingIntent receiver = (PendingIntent)intent.getParcelableExtra( + MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER); + + Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request" + + ", uri=" + uri + + ", mime=" + mime + + ", sid=" + sid + + ", pos=" + pos + + ", metadata=" + metadata + + ", headers=" + headers + + ", receiver=" + receiver); + PlaylistItem item = mSessionManager.add(uri, mime, receiver); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + result.putString(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to open " + uri.toString(), null); + } + } + mEnqueueCount +=1; + return true; + } + + private boolean handleRemove(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid == null || !sid.equals(mSessionManager.getSessionId())) { + return false; + } + + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + PlaylistItem item = mSessionManager.remove(iid); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to remove" + + ", sid=" + sid + ", iid=" + iid, null); + } + } + return (item != null); + } + + private boolean handleSeek(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + if (sid == null || !sid.equals(mSessionManager.getSessionId())) { + return false; + } + + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + long pos = intent.getLongExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, 0); + Log.d(TAG, mRouteId + ": Received seek request, pos=" + pos); + PlaylistItem item = mSessionManager.seek(iid, pos); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to seek" + + ", sid=" + sid + ", iid=" + iid + ", pos=" + pos, null); + } + } + return (item != null); + } + + private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID); + Log.d(TAG, mRouteId + ": Received getStatus request, sid=" + sid + ", iid=" + iid); + PlaylistItem item = mSessionManager.getStatus(iid); + if (callback != null) { + if (item != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get status" + + ", sid=" + sid + ", iid=" + iid, null); + } + } + return (item != null); + } + + private boolean handlePause(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.pause(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to pause, sid=" + sid, null); + } + } + return success; + } + + private boolean handleResume(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.resume(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to resume, sid=" + sid, null); + } + } + return success; + } + + private boolean handleStop(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()); + mSessionManager.stop(); + if (callback != null) { + if (success) { + callback.onResult(new Bundle()); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to stop, sid=" + sid, null); + } + } + return success; + } + + private boolean handleStartSession(Intent intent, ControlRequestCallback callback) { + String sid = mSessionManager.startSession(); + Log.d(TAG, "StartSession returns sessionId "+sid); + if (callback != null) { + if (sid != null) { + Bundle result = new Bundle(); + result.putString(MediaControlIntent.EXTRA_SESSION_ID, sid); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + callback.onResult(result); + mSessionReceiver = (PendingIntent)intent.getParcelableExtra( + MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER); + handleSessionStatusChange(sid); + } else { + callback.onError("Failed to start session.", null); + } + } + return (sid != null); + } + + private boolean handleGetSessionStatus(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + + MediaSessionStatus sessionStatus = mSessionManager.getSessionStatus(sid); + if (callback != null) { + if (sessionStatus != null) { + Bundle result = new Bundle(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + callback.onResult(result); + } else { + callback.onError("Failed to get session status, sid=" + sid, null); + } + } + return (sessionStatus != null); + } + + private boolean handleEndSession(Intent intent, ControlRequestCallback callback) { + String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID); + boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId()) + && mSessionManager.endSession(); + if (callback != null) { + if (success) { + Bundle result = new Bundle(); + MediaSessionStatus sessionStatus = new MediaSessionStatus.Builder( + MediaSessionStatus.SESSION_STATE_ENDED).build(); + result.putBundle(MediaControlIntent.EXTRA_SESSION_STATUS, sessionStatus.asBundle()); + callback.onResult(result); + handleSessionStatusChange(sid); + mSessionReceiver = null; + } else { + callback.onError("Failed to end session, sid=" + sid, null); + } + } + return success; + } + + private void handleStatusChange(PlaylistItem item) { + if (item == null) { + item = mSessionManager.getCurrentItem(); + } + if (item != null) { + PendingIntent receiver = item.getUpdateReceiver(); + if (receiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, item.getSessionId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, item.getItemId()); + intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS, + item.getStatus().asBundle()); + try { + receiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send status update!"); + } + } + } + } + + private void handleSessionStatusChange(String sid) { + if (mSessionReceiver != null) { + Intent intent = new Intent(); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sid); + intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS, + mSessionManager.getSessionStatus(sid).asBundle()); + try { + mSessionReceiver.send(getContext(), 0, intent); + Log.d(TAG, mRouteId + ": Sending session status update from provider"); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, mRouteId + ": Failed to send session status update!"); + } + } + } + } +} diff --git a/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java new file mode 100644 index 000000000..41a6cbdf4 --- /dev/null +++ b/samples/browseable/MediaRouter/src/com.example.android.mediarouter/provider/SampleMediaRouteProviderService.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.mediarouter.provider; + +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderService; + +import com.example.android.mediarouter.provider.SampleMediaRouteProvider; + +/** + * Demonstrates how to register a custom media route provider service + * using the support library. + * + * @see com.example.android.mediarouter.provider.SampleMediaRouteProvider + */ +public class SampleMediaRouteProviderService extends MediaRouteProviderService { + @Override + public MediaRouteProvider onCreateMediaRouteProvider() { + return new SampleMediaRouteProvider(this); + } +} |