Merge commit '54ec4ff7dfa9e8028f9da1986a73bddb1c00be93' into HEAD
@@ -1,3 +1,4 @@
|
||||
page.keywords="Sensor", "Games", "Accelerometer"
|
||||
page.tags="Sensor", "Games", "Accelerometer"
|
||||
sample.group=Sensors
|
||||
@jd:body
|
||||
|
||||
@@ -12,6 +12,8 @@ LOCAL_SRC_FILES += \
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := telephony-common mms-common
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES = android-support-v4
|
||||
|
||||
LOCAL_PACKAGE_NAME := ApiDemos
|
||||
|
||||
LOCAL_SDK_VERSION := current
|
||||
|
||||
@@ -996,6 +996,42 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".app.PrintBitmap"
|
||||
android:label="@string/print_bitmap"
|
||||
android:enabled="@bool/atLeastKitKat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.SAMPLE_CODE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".app.PrintHtmlFromScreen"
|
||||
android:label="@string/print_html_from_screen"
|
||||
android:enabled="@bool/atLeastKitKat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.SAMPLE_CODE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".app.PrintHtmlOffScreen"
|
||||
android:label="@string/print_html_off_screen"
|
||||
android:enabled="@bool/atLeastKitKat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.SAMPLE_CODE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".app.PrintCustomContent"
|
||||
android:label="@string/print_custom_content"
|
||||
android:enabled="@bool/atLeastKitKat">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.SAMPLE_CODE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Application Updating Samples -->
|
||||
|
||||
<!-- BEGIN_INCLUDE(app_update_declaration) -->
|
||||
|
||||
59
samples/ApiDemos/res/layout/motogp_stat_item.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:minHeight="64dip"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/year"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textIsSelectable="false">
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/champion"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:textIsSelectable="false"
|
||||
android:duplicateParentState="true">
|
||||
</TextView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/constructor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="end"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textIsSelectable="false">
|
||||
</TextView>
|
||||
|
||||
</LinearLayout>
|
||||
23
samples/ApiDemos/res/layout/print_bitmap.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/image"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:src="@raw/android_logo"
|
||||
android:contentDescription="@string/android_logo">
|
||||
</ImageView>
|
||||
22
samples/ApiDemos/res/layout/print_html_from_screen.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/web_view"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dip">
|
||||
</WebView>
|
||||
24
samples/ApiDemos/res/layout/print_html_off_screen.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textIsSelectable="false"
|
||||
android:text="@string/print_html_off_screen_msg">
|
||||
</TextView>
|
||||
21
samples/ApiDemos/res/menu/print_custom_content.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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">
|
||||
<item android:id="@+id/menu_print"
|
||||
android:title="@string/print"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
BIN
samples/ApiDemos/res/raw/android_logo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
333
samples/ApiDemos/res/raw/motogp_stats.html
Normal file
@@ -0,0 +1,333 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<table>
|
||||
<caption>500cc/MotoGP champions</caption>
|
||||
<thead><tr>
|
||||
<th scope="col">Season</td>
|
||||
<th scope="col">Rider</td>
|
||||
<th scope="col">Constructor</td>
|
||||
</tr></thead><tbody>
|
||||
<tr>
|
||||
<td>2012</td>
|
||||
<td>Jorge Lorenzo</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2011</td>
|
||||
<td>Casey Stoner</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2010</td>
|
||||
<td>Jorge Lorenzo</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2009</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2008</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2007</td>
|
||||
<td>Casey Stoner</td>
|
||||
<td>Ducati</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2006</td>
|
||||
<td>Nicky Hayden</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2005</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2004</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2003</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2002</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2001</td>
|
||||
<td>Valentino Rossi</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2000</td>
|
||||
<td>Kenny Roberts, Jr.</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1999</td>
|
||||
<td><EFBFBD>lex Crivill<6C></td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1998</td>
|
||||
<td>Michael Doohan</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1997</td>
|
||||
<td>Michael Doohan</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1996</td>
|
||||
<td>Michael Doohan</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1995</td>
|
||||
<td>Michael Doohan</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1994</td>
|
||||
<td>Michael Doohan</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1993</td>
|
||||
<td>Kevin Schwantz</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1992</td>
|
||||
<td>Wayne Rainey</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1991</td>
|
||||
<td>Wayne Rainey</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1990</td>
|
||||
<td>Wayne Rainey</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1989</td>
|
||||
<td>Eddie Lawson</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1988</td>
|
||||
<td>Eddie Lawson</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1987</td>
|
||||
<td>Wayne Gardner</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1986</td>
|
||||
<td>Eddie Lawson</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1985</td>
|
||||
<td>Freddie Spencer</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1984</td>
|
||||
<td>Eddie Lawson</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1983</td>
|
||||
<td>Freddie Spencer</td>
|
||||
<td>Honda</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1982</td>
|
||||
<td>Franco Uncini</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1981</td>
|
||||
<td>Marco Lucchinelli</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1980</td>
|
||||
<td>Kenny Roberts</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1979</td>
|
||||
<td>Kenny Roberts</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1978</td>
|
||||
<td>Kenny Roberts</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1977</td>
|
||||
<td>Barry Sheene</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1976</td>
|
||||
<td>Barry Sheene</td>
|
||||
<td>Suzuki</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1975</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>Yamaha</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1974</td>
|
||||
<td>Phil Read</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1973</td>
|
||||
<td>Phil Read</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1972</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1971</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1970</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1969</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1968</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1967</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1966</td>
|
||||
<td>Giacomo Agostini</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1965</td>
|
||||
<td>Mike Hailwood</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1964</td>
|
||||
<td>Mike Hailwood</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1963</td>
|
||||
<td>Mike Hailwood</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1962</td>
|
||||
<td>Mike Hailwood</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1961</td>
|
||||
<td>Gary Hocking</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1960</td>
|
||||
<td>John Surtees</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1959</td>
|
||||
<td>John Surtees</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1958</td>
|
||||
<td>John Surtees</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1957</td>
|
||||
<td>Libero Liberati</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1956</td>
|
||||
<td>John Surtees</td>
|
||||
<td>MV Agusta</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1955</td>
|
||||
<td>Geoff Duke</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1954</td>
|
||||
<td>Geoff Duke</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1953</td>
|
||||
<td>Geoff Duke</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1952</td>
|
||||
<td>Umberto Masetti</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1951</td>
|
||||
<td>Geoff Duke</td>
|
||||
<td>Norton</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1950</td>
|
||||
<td>Umberto Masetti</td>
|
||||
<td>Gilera</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1949</td>
|
||||
<td>Leslie Graham</td>
|
||||
<td>AJS</td>
|
||||
</tr>
|
||||
</tbody><tfoot></tfoot></table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -13,11 +13,11 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<changeBounds/>
|
||||
<fade android:fadingMode="fade_in" >
|
||||
<targets>
|
||||
<target android:targetId="@id/grayscaleContainer" />
|
||||
</targets>
|
||||
</fade>
|
||||
</set>
|
||||
</transitionSet>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<!-- BEGIN_INCLUDE(TransitionSet) -->
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:transitionOrdering="sequential">
|
||||
<changeBounds/>
|
||||
<fade android:fadingMode="fade_out" >
|
||||
@@ -22,5 +22,5 @@
|
||||
<target android:targetId="@id/grayscaleContainer" />
|
||||
</targets>
|
||||
</fade>
|
||||
</set>
|
||||
</transitionSet>
|
||||
<!-- END_INCLUDE(TransitionSet) -->
|
||||
|
||||
@@ -154,4 +154,209 @@
|
||||
<item>No.</item>
|
||||
<item>Mmm... cheese.</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Used in app/Print/Custom Layout example -->
|
||||
<string-array name="motogp_years">
|
||||
<item>2012</item>
|
||||
<item>2011</item>
|
||||
<item>2010</item>
|
||||
<item>2009</item>
|
||||
<item>2008</item>
|
||||
<item>2007</item>
|
||||
<item>2006</item>
|
||||
<item>2005</item>
|
||||
<item>2004</item>
|
||||
<item>2003</item>
|
||||
<item>2002</item>
|
||||
<item>2001</item>
|
||||
<item>2000</item>
|
||||
<item>1999</item>
|
||||
<item>1998</item>
|
||||
<item>1997</item>
|
||||
<item>1996</item>
|
||||
<item>1995</item>
|
||||
<item>1994</item>
|
||||
<item>1993</item>
|
||||
<item>1992</item>
|
||||
<item>1991</item>
|
||||
<item>1990</item>
|
||||
<item>1989</item>
|
||||
<item>1988</item>
|
||||
<item>1987</item>
|
||||
<item>1986</item>
|
||||
<item>1985</item>
|
||||
<item>1984</item>
|
||||
<item>1983</item>
|
||||
<item>1982</item>
|
||||
<item>1981</item>
|
||||
<item>1980</item>
|
||||
<item>1979</item>
|
||||
<item>1978</item>
|
||||
<item>1977</item>
|
||||
<item>1976</item>
|
||||
<item>1975</item>
|
||||
<item>1974</item>
|
||||
<item>1973</item>
|
||||
<item>1972</item>
|
||||
<item>1971</item>
|
||||
<item>1970</item>
|
||||
<item>1969</item>
|
||||
<item>1968</item>
|
||||
<item>1967</item>
|
||||
<item>1966</item>
|
||||
<item>1965</item>
|
||||
<item>1964</item>
|
||||
<item>1963</item>
|
||||
<item>1962</item>
|
||||
<item>1961</item>
|
||||
<item>1960</item>
|
||||
<item>1959</item>
|
||||
<item>1958</item>
|
||||
<item>1957</item>
|
||||
<item>1956</item>
|
||||
<item>1955</item>
|
||||
<item>1954</item>
|
||||
<item>1953</item>
|
||||
<item>1952</item>
|
||||
<item>1951</item>
|
||||
<item>1950</item>
|
||||
<item>1949</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Used in app/Print/Custom Layout example -->
|
||||
<string-array name="motogp_champions">
|
||||
<item>Jorge Lorenzo</item>
|
||||
<item>Casey Stoner</item>
|
||||
<item>Jorge Lorenzo</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Casey Stoner</item>
|
||||
<item>Nicky Hayden</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Valentino Rossi</item>
|
||||
<item>Kenny Roberts, Jr.</item>
|
||||
<item>Àlex Crivillé</item>
|
||||
<item>Michael Doohan</item>
|
||||
<item>Michael Doohan</item>
|
||||
<item>Michael Doohan</item>
|
||||
<item>Michael Doohan</item>
|
||||
<item>Michael Doohan</item>
|
||||
<item>Kevin Schwantz</item>
|
||||
<item>Wayne Rainey</item>
|
||||
<item>Wayne Rainey</item>
|
||||
<item>Wayne Rainey</item>
|
||||
<item>Eddie Lawson</item>
|
||||
<item>Eddie Lawson</item>
|
||||
<item>Wayne Gardner</item>
|
||||
<item>Eddie Lawson</item>
|
||||
<item>Freddie Spencer</item>
|
||||
<item>Eddie Lawson</item>
|
||||
<item>Freddie Spencer</item>
|
||||
<item>Franco Uncini</item>
|
||||
<item>Marco Lucchinelli</item>
|
||||
<item>Kenny Roberts</item>
|
||||
<item>Kenny Roberts</item>
|
||||
<item>Kenny Roberts</item>
|
||||
<item>Barry Sheene</item>
|
||||
<item>Barry Sheene</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Phil Read</item>
|
||||
<item>Phil Read</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Mike Hailwood</item>
|
||||
<item>Mike Hailwood</item>
|
||||
<item>Mike Hailwood</item>
|
||||
<item>Mike Hailwood</item>
|
||||
<item>Gary Hocking</item>
|
||||
<item>John Surtees</item>
|
||||
<item>John Surtees</item>
|
||||
<item>John Surtees</item>
|
||||
<item>Libero Liberati</item>
|
||||
<item>John Surtees</item>
|
||||
<item>Geoff Duke</item>
|
||||
<item>Geoff Duke</item>
|
||||
<item>Geoff Duke</item>
|
||||
<item>Umberto Masetti</item>
|
||||
<item>Geoff Duke</item>
|
||||
<item>Umberto Masetti</item>
|
||||
<item>Leslie Graham</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Used in app/Print/Custom Layout example -->
|
||||
<string-array name="motogp_constructors">
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Ducati</item>
|
||||
<item>Honda</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Honda</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Honda</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Yamaha</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Suzuki</item>
|
||||
<item>Giacomo Agostini</item>
|
||||
<item>Phil Read</item>
|
||||
<item>Phil Read</item>
|
||||
<item>Yamaha</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>Gilera</item>
|
||||
<item>MV Agusta</item>
|
||||
<item>Gilera</item>
|
||||
<item>Gilera</item>
|
||||
<item>Gilera</item>
|
||||
<item>Gilera</item>
|
||||
<item>Norton</item>
|
||||
<item>Gilera</item>
|
||||
<item>AJS</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -871,6 +871,19 @@
|
||||
<string name="btn_toggle_tabs">Toggle tab mode</string>
|
||||
<string name="btn_remove_all_tabs">Remove all tabs</string>
|
||||
|
||||
<!-- ================================= -->
|
||||
<!-- app/print print examples strings -->
|
||||
<!-- ================================= -->
|
||||
|
||||
<string name="print_bitmap">App/Print/Print Bitmap</string>
|
||||
<string name="print_html_from_screen">App/Print/Print HTML from screen</string>
|
||||
<string name="print_html_off_screen">App/Print/Print HTML off screen</string>
|
||||
<string name="print_custom_content">App/Print/Print Custom Layout</string>
|
||||
<string name="print">Print</string>
|
||||
<string name="print_html_off_screen_msg">From the overflow menu you can print some
|
||||
off screen content.</string>
|
||||
<string name="android_logo">Android logo</string>
|
||||
|
||||
<!-- ============================ -->
|
||||
<!-- graphics examples strings -->
|
||||
<!-- ============================ -->
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.example.android.apis.app;
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.print.PrintManager;
|
||||
import android.support.v4.print.PrintHelper;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
/**
|
||||
* This class demonstrates how to implement bitmap printing.
|
||||
* <p>
|
||||
* This activity shows an image and offers a print option in the overflow
|
||||
* menu. When the user chooses to print a helper class from the support
|
||||
* library is used to print the image.
|
||||
* </p>
|
||||
*
|
||||
* @see PrintManager
|
||||
* @see WebView
|
||||
*/
|
||||
public class PrintBitmap extends Activity {
|
||||
|
||||
private ImageView mImageView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.print_bitmap);
|
||||
mImageView = (ImageView) findViewById(R.id.image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.print_custom_content, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_print) {
|
||||
print();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void print() {
|
||||
// Get the print manager.
|
||||
PrintHelper printHelper = new PrintHelper(this);
|
||||
|
||||
// Set the desired scale mode.
|
||||
printHelper.setScaleMode(PrintHelper.SCALE_MODE_FIT);
|
||||
|
||||
// Get the bitmap for the ImageView's drawable.
|
||||
Bitmap bitmap = ((BitmapDrawable) mImageView.getDrawable()).getBitmap();
|
||||
|
||||
// Print the bitmap.
|
||||
printHelper.printBitmap("Print Bitmap", bitmap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
/*
|
||||
* 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.apis.app;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.pdf.PdfDocument.Page;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.CancellationSignal.OnCancelListener;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.print.PageRange;
|
||||
import android.print.PrintAttributes;
|
||||
import android.print.PrintDocumentAdapter;
|
||||
import android.print.PrintDocumentInfo;
|
||||
import android.print.PrintManager;
|
||||
import android.print.pdf.PrintedPdfDocument;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.View.MeasureSpec;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class demonstrates how to implement custom printing support.
|
||||
* <p>
|
||||
* This activity shows the list of the MotoGP champions by year and
|
||||
* brand. The print option in the overflow menu allows the user to
|
||||
* print the content. The list list of items is laid out to such that
|
||||
* it fits the options selected by the user from the UI such as page
|
||||
* size. Hence, for different page sizes the printed content will have
|
||||
* different page count.
|
||||
* </p>
|
||||
* <p>
|
||||
* This sample demonstrates how to completely implement a {@link
|
||||
* PrintDocumentAdapter} in which:
|
||||
* <ul>
|
||||
* <li>Layout based on the selected print options is performed.</li>
|
||||
* <li>Layout work is performed only if print options change would change the content.</li>
|
||||
* <li>Layout result is properly reported.</li>
|
||||
* <li>Only requested pages are written.</li>
|
||||
* <li>Write result is properly reported.</li>
|
||||
* <li>Both Layout and write respond to cancellation.</li>
|
||||
* <li>Layout and render of views is demonstrated.</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*
|
||||
* @see PrintManager
|
||||
* @see PrintDocumentAdapter
|
||||
*/
|
||||
public class PrintCustomContent extends ListActivity {
|
||||
|
||||
private static final int MILS_IN_INCH = 1000;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setListAdapter(new MotoGpStatAdapter(loadMotoGpStats(),
|
||||
getLayoutInflater()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.print_custom_content, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_print) {
|
||||
print();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void print() {
|
||||
PrintManager printManager = (PrintManager) getSystemService(
|
||||
Context.PRINT_SERVICE);
|
||||
|
||||
printManager.print("MotoGP stats",
|
||||
new PrintDocumentAdapter() {
|
||||
private int mRenderPageWidth;
|
||||
private int mRenderPageHeight;
|
||||
|
||||
private PrintAttributes mPrintAttributes;
|
||||
private PrintDocumentInfo mDocumentInfo;
|
||||
private Context mPrintContext;
|
||||
|
||||
@Override
|
||||
public void onLayout(final PrintAttributes oldAttributes,
|
||||
final PrintAttributes newAttributes,
|
||||
final CancellationSignal cancellationSignal,
|
||||
final LayoutResultCallback callback,
|
||||
final Bundle metadata) {
|
||||
|
||||
// If we are already cancelled, don't do any work.
|
||||
if (cancellationSignal.isCanceled()) {
|
||||
callback.onLayoutCancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now we determined if the print attributes changed in a way that
|
||||
// would change the layout and if so we will do a layout pass.
|
||||
boolean layoutNeeded = false;
|
||||
|
||||
final int density = Math.max(newAttributes.getResolution().getHorizontalDpi(),
|
||||
newAttributes.getResolution().getVerticalDpi());
|
||||
|
||||
// Note that we are using the PrintedPdfDocument class which creates
|
||||
// a PDF generating canvas whose size is in points (1/72") not screen
|
||||
// pixels. Hence, this canvas is pretty small compared to the screen.
|
||||
// The recommended way is to layout the content in the desired size,
|
||||
// in this case as large as the printer can do, and set a translation
|
||||
// to the PDF canvas to shrink in. Note that PDF is a vector format
|
||||
// and you will not lose data during the transformation.
|
||||
|
||||
// The content width is equal to the page width minus the margins times
|
||||
// the horizontal printer density. This way we get the maximal number
|
||||
// of pixels the printer can put horizontally.
|
||||
final int marginLeft = (int) (density * (float) newAttributes.getMinMargins()
|
||||
.getLeftMils() / MILS_IN_INCH);
|
||||
final int marginRight = (int) (density * (float) newAttributes.getMinMargins()
|
||||
.getRightMils() / MILS_IN_INCH);
|
||||
final int contentWidth = (int) (density * (float) newAttributes.getMediaSize()
|
||||
.getWidthMils() / MILS_IN_INCH) - marginLeft - marginRight;
|
||||
if (mRenderPageWidth != contentWidth) {
|
||||
mRenderPageWidth = contentWidth;
|
||||
layoutNeeded = true;
|
||||
}
|
||||
|
||||
// The content height is equal to the page height minus the margins times
|
||||
// the vertical printer resolution. This way we get the maximal number
|
||||
// of pixels the printer can put vertically.
|
||||
final int marginTop = (int) (density * (float) newAttributes.getMinMargins()
|
||||
.getTopMils() / MILS_IN_INCH);
|
||||
final int marginBottom = (int) (density * (float) newAttributes.getMinMargins()
|
||||
.getBottomMils() / MILS_IN_INCH);
|
||||
final int contentHeight = (int) (density * (float) newAttributes.getMediaSize()
|
||||
.getHeightMils() / MILS_IN_INCH) - marginTop - marginBottom;
|
||||
if (mRenderPageHeight != contentHeight) {
|
||||
mRenderPageHeight = contentHeight;
|
||||
layoutNeeded = true;
|
||||
}
|
||||
|
||||
// Create a context for resources at printer density. We will
|
||||
// be inflating views to render them and would like them to use
|
||||
// resources for a density the printer supports.
|
||||
if (mPrintContext == null || mPrintContext.getResources()
|
||||
.getConfiguration().densityDpi != density) {
|
||||
Configuration configuration = new Configuration();
|
||||
configuration.densityDpi = density;
|
||||
mPrintContext = createConfigurationContext(
|
||||
configuration);
|
||||
mPrintContext.setTheme(android.R.style.Theme_Holo_Light);
|
||||
}
|
||||
|
||||
// If no layout is needed that we did a layout at least once and
|
||||
// the document info is not null, also the second argument is false
|
||||
// to notify the system that the content did not change. This is
|
||||
// important as if the system has some pages and the content didn't
|
||||
// change the system will ask, the application to write them again.
|
||||
if (!layoutNeeded) {
|
||||
callback.onLayoutFinished(mDocumentInfo, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// For demonstration purposes we will do the layout off the main
|
||||
// thread but for small content sizes like this one it is OK to do
|
||||
// that on the main thread.
|
||||
|
||||
// Store the data as we will layout off the main thread.
|
||||
final List<MotoGpStatItem> items = ((MotoGpStatAdapter)
|
||||
getListAdapter()).cloneItems();
|
||||
|
||||
new AsyncTask<Void, Void, PrintDocumentInfo>() {
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
// First register for cancellation requests.
|
||||
cancellationSignal.setOnCancelListener(new OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel() {
|
||||
cancel(true);
|
||||
}
|
||||
});
|
||||
// Stash the attributes as we will need them for rendering.
|
||||
mPrintAttributes = newAttributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PrintDocumentInfo doInBackground(Void... params) {
|
||||
try {
|
||||
// Create an adapter with the stats and an inflater
|
||||
// to load resources for the printer density.
|
||||
MotoGpStatAdapter adapter = new MotoGpStatAdapter(items,
|
||||
(LayoutInflater) mPrintContext.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE));
|
||||
|
||||
int currentPage = 0;
|
||||
int pageContentHeight = 0;
|
||||
int viewType = -1;
|
||||
View view = null;
|
||||
LinearLayout dummyParent = new LinearLayout(mPrintContext);
|
||||
dummyParent.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final int itemCount = adapter.getCount();
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
// Be nice and respond to cancellation.
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next view.
|
||||
final int nextViewType = adapter.getItemViewType(i);
|
||||
if (viewType == nextViewType) {
|
||||
view = adapter.getView(i, view, dummyParent);
|
||||
} else {
|
||||
view = adapter.getView(i, null, dummyParent);
|
||||
}
|
||||
viewType = nextViewType;
|
||||
|
||||
// Measure the next view
|
||||
measureView(view);
|
||||
|
||||
// Add the height but if the view crosses the page
|
||||
// boundary we will put it to the next page.
|
||||
pageContentHeight += view.getMeasuredHeight();
|
||||
if (pageContentHeight > mRenderPageHeight) {
|
||||
pageContentHeight = view.getMeasuredHeight();
|
||||
currentPage++;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a document info describing the result.
|
||||
PrintDocumentInfo info = new PrintDocumentInfo
|
||||
.Builder("MotoGP_stats.pdf")
|
||||
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
|
||||
.setPageCount(currentPage + 1)
|
||||
.build();
|
||||
|
||||
// We completed the layout as a result of print attributes
|
||||
// change. Hence, if we are here the content changed for
|
||||
// sure which is why we pass true as the second argument.
|
||||
callback.onLayoutFinished(info, true);
|
||||
return info;
|
||||
} catch (Exception e) {
|
||||
// An unexpected error, report that we failed and
|
||||
// one may pass in a human readable localized text
|
||||
// for what the error is if known.
|
||||
callback.onLayoutFailed(null);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(PrintDocumentInfo result) {
|
||||
// Update the cached info to send it over if the next
|
||||
// layout pass does not result in a content change.
|
||||
mDocumentInfo = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(PrintDocumentInfo result) {
|
||||
// Task was cancelled, report that.
|
||||
callback.onLayoutCancelled();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(final PageRange[] pages,
|
||||
final ParcelFileDescriptor destination,
|
||||
final CancellationSignal cancellationSignal,
|
||||
final WriteResultCallback callback) {
|
||||
|
||||
// If we are already cancelled, don't do any work.
|
||||
if (cancellationSignal.isCanceled()) {
|
||||
callback.onWriteCancelled();
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the data as we will layout off the main thread.
|
||||
final List<MotoGpStatItem> items = ((MotoGpStatAdapter)
|
||||
getListAdapter()).cloneItems();
|
||||
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
private final SparseIntArray mWrittenPages = new SparseIntArray();
|
||||
private final PrintedPdfDocument mPdfDocument = new PrintedPdfDocument(
|
||||
PrintCustomContent.this, mPrintAttributes);
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
// First register for cancellation requests.
|
||||
cancellationSignal.setOnCancelListener(new OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel() {
|
||||
cancel(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
// Go over all the pages and write only the requested ones.
|
||||
// Create an adapter with the stats and an inflater
|
||||
// to load resources for the printer density.
|
||||
MotoGpStatAdapter adapter = new MotoGpStatAdapter(items,
|
||||
(LayoutInflater) mPrintContext.getSystemService(
|
||||
Context.LAYOUT_INFLATER_SERVICE));
|
||||
|
||||
int currentPage = -1;
|
||||
int pageContentHeight = 0;
|
||||
int viewType = -1;
|
||||
View view = null;
|
||||
Page page = null;
|
||||
LinearLayout dummyParent = new LinearLayout(mPrintContext);
|
||||
dummyParent.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
// The content is laid out and rendered in screen pixels with
|
||||
// the width and height of the paper size times the print
|
||||
// density but the PDF canvas size is in points which are 1/72",
|
||||
// so we will scale down the content.
|
||||
final float scale = Math.min(
|
||||
(float) mPdfDocument.getPageContentRect().width()
|
||||
/ mRenderPageWidth,
|
||||
(float) mPdfDocument.getPageContentRect().height()
|
||||
/ mRenderPageHeight);
|
||||
|
||||
final int itemCount = adapter.getCount();
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
// Be nice and respond to cancellation.
|
||||
if (isCancelled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the next view.
|
||||
final int nextViewType = adapter.getItemViewType(i);
|
||||
if (viewType == nextViewType) {
|
||||
view = adapter.getView(i, view, dummyParent);
|
||||
} else {
|
||||
view = adapter.getView(i, null, dummyParent);
|
||||
}
|
||||
viewType = nextViewType;
|
||||
|
||||
// Measure the next view
|
||||
measureView(view);
|
||||
|
||||
// Add the height but if the view crosses the page
|
||||
// boundary we will put it to the next one.
|
||||
pageContentHeight += view.getMeasuredHeight();
|
||||
if (currentPage < 0 || pageContentHeight > mRenderPageHeight) {
|
||||
pageContentHeight = view.getMeasuredHeight();
|
||||
currentPage++;
|
||||
// Done with the current page - finish it.
|
||||
if (page != null) {
|
||||
mPdfDocument.finishPage(page);
|
||||
}
|
||||
// If the page is requested, render it.
|
||||
if (containsPage(pages, currentPage)) {
|
||||
page = mPdfDocument.startPage(currentPage);
|
||||
page.getCanvas().scale(scale, scale);
|
||||
// Keep track which pages are written.
|
||||
mWrittenPages.append(mWrittenPages.size(), currentPage);
|
||||
} else {
|
||||
page = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If the current view is on a requested page, render it.
|
||||
if (page != null) {
|
||||
// Layout an render the content.
|
||||
view.layout(0, 0, view.getMeasuredWidth(),
|
||||
view.getMeasuredHeight());
|
||||
view.draw(page.getCanvas());
|
||||
// Move the canvas for the next view.
|
||||
page.getCanvas().translate(0, view.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
// Done with the last page.
|
||||
if (page != null) {
|
||||
mPdfDocument.finishPage(page);
|
||||
}
|
||||
|
||||
// Write the data and return success or failure.
|
||||
try {
|
||||
mPdfDocument.writeTo(new FileOutputStream(
|
||||
destination.getFileDescriptor()));
|
||||
// Compute which page ranges were written based on
|
||||
// the bookkeeping we maintained.
|
||||
PageRange[] pageRanges = computeWrittenPageRanges(mWrittenPages);
|
||||
callback.onWriteFinished(pageRanges);
|
||||
} catch (IOException ioe) {
|
||||
callback.onWriteFailed(null);
|
||||
} finally {
|
||||
mPdfDocument.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled(Void result) {
|
||||
// Task was cancelled, report that.
|
||||
callback.onWriteCancelled();
|
||||
mPdfDocument.close();
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
|
||||
}
|
||||
|
||||
private void measureView(View view) {
|
||||
final int widthMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(mRenderPageWidth,
|
||||
MeasureSpec.EXACTLY), 0, view.getLayoutParams().width);
|
||||
final int heightMeasureSpec = ViewGroup.getChildMeasureSpec(
|
||||
MeasureSpec.makeMeasureSpec(mRenderPageHeight,
|
||||
MeasureSpec.EXACTLY), 0, view.getLayoutParams().height);
|
||||
view.measure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
|
||||
private PageRange[] computeWrittenPageRanges(SparseIntArray writtenPages) {
|
||||
List<PageRange> pageRanges = new ArrayList<PageRange>();
|
||||
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
final int writtenPageCount = writtenPages.size();
|
||||
for (int i = 0; i < writtenPageCount; i++) {
|
||||
if (start < 0) {
|
||||
start = writtenPages.valueAt(i);
|
||||
}
|
||||
int oldEnd = end = start;
|
||||
while (i < writtenPageCount && (end - oldEnd) <= 1) {
|
||||
oldEnd = end;
|
||||
end = writtenPages.valueAt(i);
|
||||
i++;
|
||||
}
|
||||
PageRange pageRange = new PageRange(start, end);
|
||||
pageRanges.add(pageRange);
|
||||
start = end = -1;
|
||||
}
|
||||
|
||||
PageRange[] pageRangesArray = new PageRange[pageRanges.size()];
|
||||
pageRanges.toArray(pageRangesArray);
|
||||
return pageRangesArray;
|
||||
}
|
||||
|
||||
private boolean containsPage(PageRange[] pageRanges, int page) {
|
||||
final int pageRangeCount = pageRanges.length;
|
||||
for (int i = 0; i < pageRangeCount; i++) {
|
||||
if (pageRanges[i].getStart() <= page
|
||||
&& pageRanges[i].getEnd() >= page) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
private List<MotoGpStatItem> loadMotoGpStats() {
|
||||
String[] years = getResources().getStringArray(R.array.motogp_years);
|
||||
String[] champions = getResources().getStringArray(R.array.motogp_champions);
|
||||
String[] constructors = getResources().getStringArray(R.array.motogp_constructors);
|
||||
|
||||
List<MotoGpStatItem> items = new ArrayList<MotoGpStatItem>();
|
||||
|
||||
final int itemCount = years.length;
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
MotoGpStatItem item = new MotoGpStatItem();
|
||||
item.year = years[i];
|
||||
item.champion = champions[i];
|
||||
item.constructor = constructors[i];
|
||||
items.add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static final class MotoGpStatItem {
|
||||
String year;
|
||||
String champion;
|
||||
String constructor;
|
||||
}
|
||||
|
||||
private class MotoGpStatAdapter extends BaseAdapter {
|
||||
private final List<MotoGpStatItem> mItems;
|
||||
private final LayoutInflater mInflater;
|
||||
|
||||
public MotoGpStatAdapter(List<MotoGpStatItem> items, LayoutInflater inflater) {
|
||||
mItems = items;
|
||||
mInflater = inflater;
|
||||
}
|
||||
|
||||
public List<MotoGpStatItem> cloneItems() {
|
||||
return new ArrayList<MotoGpStatItem>(mItems);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mItems.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = mInflater.inflate(R.layout.motogp_stat_item, parent, false);
|
||||
}
|
||||
|
||||
MotoGpStatItem item = (MotoGpStatItem) getItem(position);
|
||||
|
||||
TextView yearView = (TextView) convertView.findViewById(R.id.year);
|
||||
yearView.setText(item.year);
|
||||
|
||||
TextView championView = (TextView) convertView.findViewById(R.id.champion);
|
||||
championView.setText(item.champion);
|
||||
|
||||
TextView constructorView = (TextView) convertView.findViewById(R.id.constructor);
|
||||
constructorView.setText(item.constructor);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.apis.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.print.PrintManager;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
/**
|
||||
* This class demonstrates how to implement HTML content printing
|
||||
* from a {@link WebView} which is shown on the screen.
|
||||
* <p>
|
||||
* This activity shows a simple HTML content in a {@link WebView}
|
||||
* and allows the user to print that content via an action in the
|
||||
* action bar. The shown {@link WebView} is doing the printing.
|
||||
* </p>
|
||||
*
|
||||
* @see PrintManager
|
||||
* @see WebView
|
||||
*/
|
||||
public class PrintHtmlFromScreen extends Activity {
|
||||
|
||||
private WebView mWebView;
|
||||
|
||||
private boolean mDataLoaded;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.print_html_from_screen);
|
||||
mWebView = (WebView) findViewById(R.id.web_view);
|
||||
|
||||
// Important: Only enable the print option after the page is loaded.
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
// Data loaded, so now we want to show the print option.
|
||||
mDataLoaded = true;
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Load an HTML page.
|
||||
mWebView.loadUrl("file:///android_res/raw/motogp_stats.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
if (mDataLoaded) {
|
||||
getMenuInflater().inflate(R.menu.print_custom_content, menu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_print) {
|
||||
print();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void print() {
|
||||
// Get the print manager.
|
||||
PrintManager printManager = (PrintManager) getSystemService(
|
||||
Context.PRINT_SERVICE);
|
||||
// Pass in the ViewView's document adapter.
|
||||
printManager.print("MotoGP stats", mWebView.createPrintDocumentAdapter(), null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.apis.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.print.PageRange;
|
||||
import android.print.PrintAttributes;
|
||||
import android.print.PrintDocumentAdapter;
|
||||
import android.print.PrintManager;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.example.android.apis.R;
|
||||
|
||||
/**
|
||||
* This class demonstrates how to implement HTML content printing
|
||||
* from a {@link WebView} which is not shown on the screen.
|
||||
* <p>
|
||||
* This activity shows a text prompt and when the user chooses the
|
||||
* print option from the overflow menu an HTML page with content that
|
||||
* is not on the screen is printed via an off-screen {@link WebView}.
|
||||
* </p>
|
||||
*
|
||||
* @see PrintManager
|
||||
* @see WebView
|
||||
*/
|
||||
public class PrintHtmlOffScreen extends Activity {
|
||||
|
||||
private WebView mWebView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.print_html_off_screen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
getMenuInflater().inflate(R.menu.print_custom_content, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_print) {
|
||||
print();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void print() {
|
||||
// Create a WebView and hold on to it as the printing will start when
|
||||
// load completes and we do not want the WbeView to be garbage collected.
|
||||
mWebView = new WebView(this);
|
||||
|
||||
// Important: Only after the page is loaded we will do the print.
|
||||
mWebView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
doPrint();
|
||||
}
|
||||
});
|
||||
|
||||
// Load an HTML page.
|
||||
mWebView.loadUrl("file:///android_res/raw/motogp_stats.html");
|
||||
}
|
||||
|
||||
private void doPrint() {
|
||||
// Get the print manager.
|
||||
PrintManager printManager = (PrintManager) getSystemService(
|
||||
Context.PRINT_SERVICE);
|
||||
|
||||
// Create a wrapper PrintDocumentAdapter to clean up when done.
|
||||
PrintDocumentAdapter adapter = new PrintDocumentAdapter() {
|
||||
private final PrintDocumentAdapter mWrappedInstance =
|
||||
mWebView.createPrintDocumentAdapter();
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
mWrappedInstance.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
|
||||
CancellationSignal cancellationSignal, LayoutResultCallback callback,
|
||||
Bundle extras) {
|
||||
mWrappedInstance.onLayout(oldAttributes, newAttributes, cancellationSignal,
|
||||
callback, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
|
||||
CancellationSignal cancellationSignal, WriteResultCallback callback) {
|
||||
mWrappedInstance.onWrite(pages, destination, cancellationSignal, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
mWrappedInstance.onFinish();
|
||||
// Intercept the finish call to know when printing is done
|
||||
// and destroy the WebView as it is expensive to keep around.
|
||||
mWebView.destroy();
|
||||
mWebView = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Pass in the ViewView's document adapter.
|
||||
printManager.print("MotoGP stats", adapter, null);
|
||||
}
|
||||
}
|
||||
9
samples/OpenGL/HelloOpenGLES10/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Files ignored for git source control
|
||||
#
|
||||
# Add this file to source control so the following files
|
||||
# are not tracked and added to git:
|
||||
.project
|
||||
.classpath
|
||||
project.properties
|
||||
bin/
|
||||
gen/
|
||||
44
samples/OpenGL/HelloOpenGLES10/AndroidManifest.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.opengl"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="4"
|
||||
android:targetSdkVersion="17" />
|
||||
|
||||
<!-- Tell the system this app requires OpenGL ES 1.0 or higher-->
|
||||
<uses-feature android:glEsVersion="0x00010000" />
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:allowBackup="true" >
|
||||
<activity
|
||||
android:name="com.example.android.opengl.OpenGLES10Activity"
|
||||
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>
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
28
samples/OpenGL/HelloOpenGLES10/res/layout/main.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
</LinearLayout>
|
||||
20
samples/OpenGL/HelloOpenGLES10/res/values/strings.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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.
|
||||
-->
|
||||
<resources>
|
||||
<string name="hello">Hello, OpenGL ES 1.0!</string>
|
||||
<string name="app_name">Hello OpenGL ES 1.0</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.opengl.GLU;
|
||||
|
||||
/**
|
||||
* Provides drawing instructions for a GLSurfaceView object. This class
|
||||
* must override the OpenGL ES drawing lifecycle methods:
|
||||
* <ul>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onSurfaceCreated}</li>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onDrawFrame}</li>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onSurfaceChanged}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class MyGLRenderer implements GLSurfaceView.Renderer {
|
||||
|
||||
private Triangle mTriangle;
|
||||
private Square mSquare;
|
||||
private float mAngle;
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
// Set the background frame color
|
||||
gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
mTriangle = new Triangle();
|
||||
mSquare = new Square();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 gl) {
|
||||
|
||||
// Draw background color
|
||||
gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Set GL_MODELVIEW transformation mode
|
||||
gl.glMatrixMode(GL10.GL_MODELVIEW);
|
||||
gl.glLoadIdentity(); // reset the matrix to its default state
|
||||
|
||||
// When using GL_MODELVIEW, you must set the view point
|
||||
GLU.gluLookAt(gl, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
|
||||
|
||||
// Draw square
|
||||
mSquare.draw(gl);
|
||||
|
||||
// Create a rotation for the triangle
|
||||
|
||||
// Use the following code to generate constant rotation.
|
||||
// Leave this code out when using TouchEvents.
|
||||
// long time = SystemClock.uptimeMillis() % 4000L;
|
||||
// float angle = 0.090f * ((int) time);
|
||||
|
||||
gl.glRotatef(mAngle, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
// Draw triangle
|
||||
mTriangle.draw(gl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 gl, int width, int height) {
|
||||
// Adjust the viewport based on geometry changes
|
||||
// such as screen rotations
|
||||
gl.glViewport(0, 0, width, height);
|
||||
|
||||
// make adjustments for screen ratio
|
||||
float ratio = (float) width / height;
|
||||
gl.glMatrixMode(GL10.GL_PROJECTION); // set matrix to projection mode
|
||||
gl.glLoadIdentity(); // reset the matrix to its default state
|
||||
gl.glFrustumf(-ratio, ratio, -1, 1, 3, 7); // apply the projection matrix
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotation angle of the triangle shape (mTriangle).
|
||||
*
|
||||
* @return - A float representing the rotation angle.
|
||||
*/
|
||||
public float getAngle() {
|
||||
return mAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rotation angle of the triangle shape (mTriangle).
|
||||
*/
|
||||
public void setAngle(float angle) {
|
||||
mAngle = angle;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* A view container where OpenGL ES graphics can be drawn on screen.
|
||||
* This view can also be used to capture touch events, such as a user
|
||||
* interacting with drawn objects.
|
||||
*/
|
||||
public class MyGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
private final MyGLRenderer mRenderer;
|
||||
|
||||
public MyGLSurfaceView(Context context) {
|
||||
super(context);
|
||||
|
||||
// Set the Renderer for drawing on the GLSurfaceView
|
||||
mRenderer = new MyGLRenderer();
|
||||
setRenderer(mRenderer);
|
||||
|
||||
// Render the view only when there is a change in the drawing data
|
||||
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||
}
|
||||
|
||||
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
|
||||
private float mPreviousX;
|
||||
private float mPreviousY;
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
// MotionEvent reports input details from the touch screen
|
||||
// and other input controls. In this case, we are only
|
||||
// interested in events where the touch position changed.
|
||||
|
||||
float x = e.getX();
|
||||
float y = e.getY();
|
||||
|
||||
switch (e.getAction()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
|
||||
float dx = x - mPreviousX;
|
||||
float dy = y - mPreviousY;
|
||||
|
||||
// reverse direction of rotation above the mid-line
|
||||
if (y > getHeight() / 2) {
|
||||
dx = dx * -1 ;
|
||||
}
|
||||
|
||||
// reverse direction of rotation to left of the mid-line
|
||||
if (x < getWidth() / 2) {
|
||||
dy = dy * -1 ;
|
||||
}
|
||||
|
||||
mRenderer.setAngle(
|
||||
mRenderer.getAngle() +
|
||||
((dx + dy) * TOUCH_SCALE_FACTOR)); // = 180.0f / 320
|
||||
requestRender();
|
||||
}
|
||||
|
||||
mPreviousX = x;
|
||||
mPreviousY = y;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class OpenGLES10Activity extends Activity {
|
||||
|
||||
private GLSurfaceView mGLView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Create a GLSurfaceView instance and set it
|
||||
// as the ContentView for this Activity.
|
||||
mGLView = new MyGLSurfaceView(this);
|
||||
setContentView(mGLView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
// The following call pauses the rendering thread.
|
||||
// If your OpenGL application is memory intensive,
|
||||
// you should consider de-allocating objects that
|
||||
// consume significant memory here.
|
||||
super.onPause();
|
||||
mGLView.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
// The following call resumes a paused rendering thread.
|
||||
// If you de-allocated graphic objects for onPause()
|
||||
// this is a good place to re-allocate them.
|
||||
super.onResume();
|
||||
mGLView.onResume();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* A two-dimensional square for use as a drawn object in OpenGL ES 1.0/1.1.
|
||||
*/
|
||||
public class Square {
|
||||
|
||||
private final FloatBuffer vertexBuffer;
|
||||
private final ShortBuffer drawListBuffer;
|
||||
|
||||
// number of coordinates per vertex in this array
|
||||
static final int COORDS_PER_VERTEX = 3;
|
||||
static float squareCoords[] = {
|
||||
-0.5f, 0.5f, 0.0f, // top left
|
||||
-0.5f, -0.5f, 0.0f, // bottom left
|
||||
0.5f, -0.5f, 0.0f, // bottom right
|
||||
0.5f, 0.5f, 0.0f }; // top right
|
||||
|
||||
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
|
||||
|
||||
float color[] = { 0.2f, 0.709803922f, 0.898039216f, 1.0f };
|
||||
|
||||
/**
|
||||
* Sets up the drawing object data for use in an OpenGL ES context.
|
||||
*/
|
||||
public Square() {
|
||||
// initialize vertex byte buffer for shape coordinates
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect(
|
||||
// (# of coordinate values * 4 bytes per float)
|
||||
squareCoords.length * 4);
|
||||
bb.order(ByteOrder.nativeOrder());
|
||||
vertexBuffer = bb.asFloatBuffer();
|
||||
vertexBuffer.put(squareCoords);
|
||||
vertexBuffer.position(0);
|
||||
|
||||
// initialize byte buffer for the draw list
|
||||
ByteBuffer dlb = ByteBuffer.allocateDirect(
|
||||
// (# of coordinate values * 2 bytes per short)
|
||||
drawOrder.length * 2);
|
||||
dlb.order(ByteOrder.nativeOrder());
|
||||
drawListBuffer = dlb.asShortBuffer();
|
||||
drawListBuffer.put(drawOrder);
|
||||
drawListBuffer.position(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the OpenGL ES instructions for drawing this shape.
|
||||
*
|
||||
* @param gl - The OpenGL ES context in which to draw this shape.
|
||||
*/
|
||||
public void draw(GL10 gl) {
|
||||
// Since this shape uses vertex arrays, enable them
|
||||
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
|
||||
|
||||
// draw the shape
|
||||
gl.glColor4f( // set color
|
||||
color[0], color[1],
|
||||
color[2], color[3]);
|
||||
gl.glVertexPointer( // point to vertex data:
|
||||
COORDS_PER_VERTEX,
|
||||
GL10.GL_FLOAT, 0, vertexBuffer);
|
||||
gl.glDrawElements( // draw shape:
|
||||
GL10.GL_TRIANGLES,
|
||||
drawOrder.length, GL10.GL_UNSIGNED_SHORT,
|
||||
drawListBuffer);
|
||||
|
||||
// Disable vertex array drawing to avoid
|
||||
// conflicts with shapes that don't use it
|
||||
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
/**
|
||||
* A two-dimensional triangle for use as a drawn object in OpenGL ES 1.0/1.1.
|
||||
*/
|
||||
public class Triangle {
|
||||
|
||||
private final FloatBuffer vertexBuffer;
|
||||
|
||||
// number of coordinates per vertex in this array
|
||||
static final int COORDS_PER_VERTEX = 3;
|
||||
static float triangleCoords[] = {
|
||||
// in counterclockwise order:
|
||||
0.0f, 0.622008459f, 0.0f,// top
|
||||
-0.5f, -0.311004243f, 0.0f,// bottom left
|
||||
0.5f, -0.311004243f, 0.0f // bottom right
|
||||
};
|
||||
|
||||
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 0.0f };
|
||||
|
||||
/**
|
||||
* Sets up the drawing object data for use in an OpenGL ES context.
|
||||
*/
|
||||
public Triangle() {
|
||||
// initialize vertex byte buffer for shape coordinates
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect(
|
||||
// (number of coordinate values * 4 bytes per float)
|
||||
triangleCoords.length * 4);
|
||||
// use the device hardware's native byte order
|
||||
bb.order(ByteOrder.nativeOrder());
|
||||
|
||||
// create a floating point buffer from the ByteBuffer
|
||||
vertexBuffer = bb.asFloatBuffer();
|
||||
// add the coordinates to the FloatBuffer
|
||||
vertexBuffer.put(triangleCoords);
|
||||
// set the buffer to read the first coordinate
|
||||
vertexBuffer.position(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the OpenGL ES instructions for drawing this shape.
|
||||
*
|
||||
* @param gl - The OpenGL ES context in which to draw this shape.
|
||||
*/
|
||||
public void draw(GL10 gl) {
|
||||
// Since this shape uses vertex arrays, enable them
|
||||
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
|
||||
|
||||
// draw the shape
|
||||
gl.glColor4f( // set color:
|
||||
color[0], color[1],
|
||||
color[2], color[3]);
|
||||
gl.glVertexPointer( // point to vertex data:
|
||||
COORDS_PER_VERTEX,
|
||||
GL10.GL_FLOAT, 0, vertexBuffer);
|
||||
gl.glDrawArrays( // draw shape:
|
||||
GL10.GL_TRIANGLES, 0,
|
||||
triangleCoords.length / COORDS_PER_VERTEX);
|
||||
|
||||
// Disable vertex array drawing to avoid
|
||||
// conflicts with shapes that don't use it
|
||||
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
|
||||
}
|
||||
}
|
||||
9
samples/OpenGL/HelloOpenGLES20/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Files ignored for git source control
|
||||
#
|
||||
# Add this file to source control so the following files
|
||||
# are not tracked and added to git:
|
||||
.project
|
||||
.classpath
|
||||
project.properties
|
||||
bin/
|
||||
gen/
|
||||
44
samples/OpenGL/HelloOpenGLES20/AndroidManifest.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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.
|
||||
-->
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.opengl"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="8"
|
||||
android:targetSdkVersion="17" />
|
||||
|
||||
<!-- Tell the system this app requires OpenGL ES 2.0. -->
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:allowBackup="true" >
|
||||
<activity
|
||||
android:name="com.example.android.opengl.OpenGLES20Activity"
|
||||
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>
|
||||
BIN
samples/OpenGL/HelloOpenGLES20/res/drawable/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
28
samples/OpenGL/HelloOpenGLES20/res/layout/main.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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.
|
||||
-->
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="vertical" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hello" />
|
||||
|
||||
</LinearLayout>
|
||||
22
samples/OpenGL/HelloOpenGLES20/res/values/strings.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?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.
|
||||
-->
|
||||
<resources>
|
||||
|
||||
<string name="hello">Hello, OpenGL ES 2.0!</string>
|
||||
<string name="app_name">Hello OpenGL ES 2.0</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.opengl.Matrix;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Provides drawing instructions for a GLSurfaceView object. This class
|
||||
* must override the OpenGL ES drawing lifecycle methods:
|
||||
* <ul>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onSurfaceCreated}</li>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onDrawFrame}</li>
|
||||
* <li>{@link android.opengl.GLSurfaceView.Renderer#onSurfaceChanged}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class MyGLRenderer implements GLSurfaceView.Renderer {
|
||||
|
||||
private static final String TAG = "MyGLRenderer";
|
||||
private Triangle mTriangle;
|
||||
private Square mSquare;
|
||||
|
||||
// mMVPMatrix is an abbreviation for "Model View Projection Matrix"
|
||||
private final float[] mMVPMatrix = new float[16];
|
||||
private final float[] mProjectionMatrix = new float[16];
|
||||
private final float[] mViewMatrix = new float[16];
|
||||
private final float[] mRotationMatrix = new float[16];
|
||||
|
||||
private float mAngle;
|
||||
|
||||
@Override
|
||||
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
|
||||
|
||||
// Set the background frame color
|
||||
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
mTriangle = new Triangle();
|
||||
mSquare = new Square();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawFrame(GL10 unused) {
|
||||
float[] scratch = new float[16];
|
||||
|
||||
// Draw background color
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
// Set the camera position (View matrix)
|
||||
Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
|
||||
|
||||
// Calculate the projection and view transformation
|
||||
Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);
|
||||
|
||||
// Draw square
|
||||
mSquare.draw(mMVPMatrix);
|
||||
|
||||
// Create a rotation for the triangle
|
||||
|
||||
// Use the following code to generate constant rotation.
|
||||
// Leave this code out when using TouchEvents.
|
||||
// long time = SystemClock.uptimeMillis() % 4000L;
|
||||
// float angle = 0.090f * ((int) time);
|
||||
|
||||
Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);
|
||||
|
||||
// Combine the rotation matrix with the projection and camera view
|
||||
// Note that the mMVPMatrix factor *must be first* in order
|
||||
// for the matrix multiplication product to be correct.
|
||||
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);
|
||||
|
||||
// Draw triangle
|
||||
mTriangle.draw(scratch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceChanged(GL10 unused, int width, int height) {
|
||||
// Adjust the viewport based on geometry changes,
|
||||
// such as screen rotation
|
||||
GLES20.glViewport(0, 0, width, height);
|
||||
|
||||
float ratio = (float) width / height;
|
||||
|
||||
// this projection matrix is applied to object coordinates
|
||||
// in the onDrawFrame() method
|
||||
Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for compiling a OpenGL shader.
|
||||
*
|
||||
* <p><strong>Note:</strong> When developing shaders, use the checkGlError()
|
||||
* method to debug shader coding errors.</p>
|
||||
*
|
||||
* @param type - Vertex or fragment shader type.
|
||||
* @param shaderCode - String containing the shader code.
|
||||
* @return - Returns an id for the shader.
|
||||
*/
|
||||
public static int loadShader(int type, String shaderCode){
|
||||
|
||||
// create a vertex shader type (GLES20.GL_VERTEX_SHADER)
|
||||
// or a fragment shader type (GLES20.GL_FRAGMENT_SHADER)
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
|
||||
// add the source code to the shader and compile it
|
||||
GLES20.glShaderSource(shader, shaderCode);
|
||||
GLES20.glCompileShader(shader);
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method for debugging OpenGL calls. Provide the name of the call
|
||||
* just after making it:
|
||||
*
|
||||
* <pre>
|
||||
* mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
|
||||
* MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
|
||||
*
|
||||
* If the operation is not successful, the check throws an error.
|
||||
*
|
||||
* @param glOperation - Name of the OpenGL call to check.
|
||||
*/
|
||||
public static void checkGlError(String glOperation) {
|
||||
int error;
|
||||
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
|
||||
Log.e(TAG, glOperation + ": glError " + error);
|
||||
throw new RuntimeException(glOperation + ": glError " + error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rotation angle of the triangle shape (mTriangle).
|
||||
*
|
||||
* @return - A float representing the rotation angle.
|
||||
*/
|
||||
public float getAngle() {
|
||||
return mAngle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rotation angle of the triangle shape (mTriangle).
|
||||
*/
|
||||
public void setAngle(float angle) {
|
||||
mAngle = angle;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
/**
|
||||
* A view container where OpenGL ES graphics can be drawn on screen.
|
||||
* This view can also be used to capture touch events, such as a user
|
||||
* interacting with drawn objects.
|
||||
*/
|
||||
public class MyGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
private final MyGLRenderer mRenderer;
|
||||
|
||||
public MyGLSurfaceView(Context context) {
|
||||
super(context);
|
||||
|
||||
// Create an OpenGL ES 2.0 context.
|
||||
setEGLContextClientVersion(2);
|
||||
|
||||
// Set the Renderer for drawing on the GLSurfaceView
|
||||
mRenderer = new MyGLRenderer();
|
||||
setRenderer(mRenderer);
|
||||
|
||||
// Render the view only when there is a change in the drawing data
|
||||
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
|
||||
}
|
||||
|
||||
private final float TOUCH_SCALE_FACTOR = 180.0f / 320;
|
||||
private float mPreviousX;
|
||||
private float mPreviousY;
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent e) {
|
||||
// MotionEvent reports input details from the touch screen
|
||||
// and other input controls. In this case, you are only
|
||||
// interested in events where the touch position changed.
|
||||
|
||||
float x = e.getX();
|
||||
float y = e.getY();
|
||||
|
||||
switch (e.getAction()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
|
||||
float dx = x - mPreviousX;
|
||||
float dy = y - mPreviousY;
|
||||
|
||||
// reverse direction of rotation above the mid-line
|
||||
if (y > getHeight() / 2) {
|
||||
dx = dx * -1 ;
|
||||
}
|
||||
|
||||
// reverse direction of rotation to left of the mid-line
|
||||
if (x < getWidth() / 2) {
|
||||
dy = dy * -1 ;
|
||||
}
|
||||
|
||||
mRenderer.setAngle(
|
||||
mRenderer.getAngle() +
|
||||
((dx + dy) * TOUCH_SCALE_FACTOR)); // = 180.0f / 320
|
||||
requestRender();
|
||||
}
|
||||
|
||||
mPreviousX = x;
|
||||
mPreviousY = y;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.os.Bundle;
|
||||
|
||||
public class OpenGLES20Activity extends Activity {
|
||||
|
||||
private GLSurfaceView mGLView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Create a GLSurfaceView instance and set it
|
||||
// as the ContentView for this Activity
|
||||
mGLView = new MyGLSurfaceView(this);
|
||||
setContentView(mGLView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
// The following call pauses the rendering thread.
|
||||
// If your OpenGL application is memory intensive,
|
||||
// you should consider de-allocating objects that
|
||||
// consume significant memory here.
|
||||
mGLView.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// The following call resumes a paused rendering thread.
|
||||
// If you de-allocated graphic objects for onPause()
|
||||
// this is a good place to re-allocate them.
|
||||
mGLView.onResume();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
|
||||
/**
|
||||
* A two-dimensional square for use as a drawn object in OpenGL ES 2.0.
|
||||
*/
|
||||
public class Square {
|
||||
|
||||
private final String vertexShaderCode =
|
||||
// This matrix member variable provides a hook to manipulate
|
||||
// the coordinates of the objects that use this vertex shader
|
||||
"uniform mat4 uMVPMatrix;" +
|
||||
"attribute vec4 vPosition;" +
|
||||
"void main() {" +
|
||||
// The matrix must be included as a modifier of gl_Position.
|
||||
// Note that the uMVPMatrix factor *must be first* in order
|
||||
// for the matrix multiplication product to be correct.
|
||||
" gl_Position = uMVPMatrix * vPosition;" +
|
||||
"}";
|
||||
|
||||
private final String fragmentShaderCode =
|
||||
"precision mediump float;" +
|
||||
"uniform vec4 vColor;" +
|
||||
"void main() {" +
|
||||
" gl_FragColor = vColor;" +
|
||||
"}";
|
||||
|
||||
private final FloatBuffer vertexBuffer;
|
||||
private final ShortBuffer drawListBuffer;
|
||||
private final int mProgram;
|
||||
private int mPositionHandle;
|
||||
private int mColorHandle;
|
||||
private int mMVPMatrixHandle;
|
||||
|
||||
// number of coordinates per vertex in this array
|
||||
static final int COORDS_PER_VERTEX = 3;
|
||||
static float squareCoords[] = {
|
||||
-0.5f, 0.5f, 0.0f, // top left
|
||||
-0.5f, -0.5f, 0.0f, // bottom left
|
||||
0.5f, -0.5f, 0.0f, // bottom right
|
||||
0.5f, 0.5f, 0.0f }; // top right
|
||||
|
||||
private final short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // order to draw vertices
|
||||
|
||||
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
|
||||
|
||||
float color[] = { 0.2f, 0.709803922f, 0.898039216f, 1.0f };
|
||||
|
||||
/**
|
||||
* Sets up the drawing object data for use in an OpenGL ES context.
|
||||
*/
|
||||
public Square() {
|
||||
// initialize vertex byte buffer for shape coordinates
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect(
|
||||
// (# of coordinate values * 4 bytes per float)
|
||||
squareCoords.length * 4);
|
||||
bb.order(ByteOrder.nativeOrder());
|
||||
vertexBuffer = bb.asFloatBuffer();
|
||||
vertexBuffer.put(squareCoords);
|
||||
vertexBuffer.position(0);
|
||||
|
||||
// initialize byte buffer for the draw list
|
||||
ByteBuffer dlb = ByteBuffer.allocateDirect(
|
||||
// (# of coordinate values * 2 bytes per short)
|
||||
drawOrder.length * 2);
|
||||
dlb.order(ByteOrder.nativeOrder());
|
||||
drawListBuffer = dlb.asShortBuffer();
|
||||
drawListBuffer.put(drawOrder);
|
||||
drawListBuffer.position(0);
|
||||
|
||||
// prepare shaders and OpenGL program
|
||||
int vertexShader = MyGLRenderer.loadShader(
|
||||
GLES20.GL_VERTEX_SHADER,
|
||||
vertexShaderCode);
|
||||
int fragmentShader = MyGLRenderer.loadShader(
|
||||
GLES20.GL_FRAGMENT_SHADER,
|
||||
fragmentShaderCode);
|
||||
|
||||
mProgram = GLES20.glCreateProgram(); // create empty OpenGL Program
|
||||
GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
|
||||
GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
|
||||
GLES20.glLinkProgram(mProgram); // create OpenGL program executables
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the OpenGL ES instructions for drawing this shape.
|
||||
*
|
||||
* @param mvpMatrix - The Model View Project matrix in which to draw
|
||||
* this shape.
|
||||
*/
|
||||
public void draw(float[] mvpMatrix) {
|
||||
// Add program to OpenGL environment
|
||||
GLES20.glUseProgram(mProgram);
|
||||
|
||||
// get handle to vertex shader's vPosition member
|
||||
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
|
||||
|
||||
// Enable a handle to the triangle vertices
|
||||
GLES20.glEnableVertexAttribArray(mPositionHandle);
|
||||
|
||||
// Prepare the triangle coordinate data
|
||||
GLES20.glVertexAttribPointer(
|
||||
mPositionHandle, COORDS_PER_VERTEX,
|
||||
GLES20.GL_FLOAT, false,
|
||||
vertexStride, vertexBuffer);
|
||||
|
||||
// get handle to fragment shader's vColor member
|
||||
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
|
||||
|
||||
// Set color for drawing the triangle
|
||||
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
|
||||
|
||||
// get handle to shape's transformation matrix
|
||||
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
|
||||
MyGLRenderer.checkGlError("glGetUniformLocation");
|
||||
|
||||
// Apply the projection and view transformation
|
||||
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
|
||||
MyGLRenderer.checkGlError("glUniformMatrix4fv");
|
||||
|
||||
// Draw the square
|
||||
GLES20.glDrawElements(
|
||||
GLES20.GL_TRIANGLES, drawOrder.length,
|
||||
GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
|
||||
|
||||
// Disable vertex array
|
||||
GLES20.glDisableVertexAttribArray(mPositionHandle);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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.opengl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
import android.opengl.GLES20;
|
||||
|
||||
/**
|
||||
* A two-dimensional triangle for use as a drawn object in OpenGL ES 2.0.
|
||||
*/
|
||||
public class Triangle {
|
||||
|
||||
private final String vertexShaderCode =
|
||||
// This matrix member variable provides a hook to manipulate
|
||||
// the coordinates of the objects that use this vertex shader
|
||||
"uniform mat4 uMVPMatrix;" +
|
||||
"attribute vec4 vPosition;" +
|
||||
"void main() {" +
|
||||
// the matrix must be included as a modifier of gl_Position
|
||||
// Note that the uMVPMatrix factor *must be first* in order
|
||||
// for the matrix multiplication product to be correct.
|
||||
" gl_Position = uMVPMatrix * vPosition;" +
|
||||
"}";
|
||||
|
||||
private final String fragmentShaderCode =
|
||||
"precision mediump float;" +
|
||||
"uniform vec4 vColor;" +
|
||||
"void main() {" +
|
||||
" gl_FragColor = vColor;" +
|
||||
"}";
|
||||
|
||||
private final FloatBuffer vertexBuffer;
|
||||
private final int mProgram;
|
||||
private int mPositionHandle;
|
||||
private int mColorHandle;
|
||||
private int mMVPMatrixHandle;
|
||||
|
||||
// number of coordinates per vertex in this array
|
||||
static final int COORDS_PER_VERTEX = 3;
|
||||
static float triangleCoords[] = {
|
||||
// in counterclockwise order:
|
||||
0.0f, 0.622008459f, 0.0f, // top
|
||||
-0.5f, -0.311004243f, 0.0f, // bottom left
|
||||
0.5f, -0.311004243f, 0.0f // bottom right
|
||||
};
|
||||
private final int vertexCount = triangleCoords.length / COORDS_PER_VERTEX;
|
||||
private final int vertexStride = COORDS_PER_VERTEX * 4; // 4 bytes per vertex
|
||||
|
||||
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 0.0f };
|
||||
|
||||
/**
|
||||
* Sets up the drawing object data for use in an OpenGL ES context.
|
||||
*/
|
||||
public Triangle() {
|
||||
// initialize vertex byte buffer for shape coordinates
|
||||
ByteBuffer bb = ByteBuffer.allocateDirect(
|
||||
// (number of coordinate values * 4 bytes per float)
|
||||
triangleCoords.length * 4);
|
||||
// use the device hardware's native byte order
|
||||
bb.order(ByteOrder.nativeOrder());
|
||||
|
||||
// create a floating point buffer from the ByteBuffer
|
||||
vertexBuffer = bb.asFloatBuffer();
|
||||
// add the coordinates to the FloatBuffer
|
||||
vertexBuffer.put(triangleCoords);
|
||||
// set the buffer to read the first coordinate
|
||||
vertexBuffer.position(0);
|
||||
|
||||
// prepare shaders and OpenGL program
|
||||
int vertexShader = MyGLRenderer.loadShader(
|
||||
GLES20.GL_VERTEX_SHADER, vertexShaderCode);
|
||||
int fragmentShader = MyGLRenderer.loadShader(
|
||||
GLES20.GL_FRAGMENT_SHADER, fragmentShaderCode);
|
||||
|
||||
mProgram = GLES20.glCreateProgram(); // create empty OpenGL Program
|
||||
GLES20.glAttachShader(mProgram, vertexShader); // add the vertex shader to program
|
||||
GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
|
||||
GLES20.glLinkProgram(mProgram); // create OpenGL program executables
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the OpenGL ES instructions for drawing this shape.
|
||||
*
|
||||
* @param mvpMatrix - The Model View Project matrix in which to draw
|
||||
* this shape.
|
||||
*/
|
||||
public void draw(float[] mvpMatrix) {
|
||||
// Add program to OpenGL environment
|
||||
GLES20.glUseProgram(mProgram);
|
||||
|
||||
// get handle to vertex shader's vPosition member
|
||||
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
|
||||
|
||||
// Enable a handle to the triangle vertices
|
||||
GLES20.glEnableVertexAttribArray(mPositionHandle);
|
||||
|
||||
// Prepare the triangle coordinate data
|
||||
GLES20.glVertexAttribPointer(
|
||||
mPositionHandle, COORDS_PER_VERTEX,
|
||||
GLES20.GL_FLOAT, false,
|
||||
vertexStride, vertexBuffer);
|
||||
|
||||
// get handle to fragment shader's vColor member
|
||||
mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor");
|
||||
|
||||
// Set color for drawing the triangle
|
||||
GLES20.glUniform4fv(mColorHandle, 1, color, 0);
|
||||
|
||||
// get handle to shape's transformation matrix
|
||||
mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
|
||||
MyGLRenderer.checkGlError("glGetUniformLocation");
|
||||
|
||||
// Apply the projection and view transformation
|
||||
GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);
|
||||
MyGLRenderer.checkGlError("glUniformMatrix4fv");
|
||||
|
||||
// Draw the triangle
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
|
||||
|
||||
// Disable vertex array
|
||||
GLES20.glDisableVertexAttribArray(mPositionHandle);
|
||||
}
|
||||
|
||||
}
|
||||
58
samples/Support7Demos/res/layout/sample_media_controller.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<?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 the customized MediaRouteControllerDialog -->
|
||||
|
||||
<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:id="@+id/media_route_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
<ImageView
|
||||
android:id="@+id/snapshot"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:scaleType="centerCrop"/>
|
||||
<TextView android:id="@+id/track_info"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="4dp"/>
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:gravity="center">
|
||||
<ImageButton android:id="@+id/pause_resume_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_media_pause" />
|
||||
<ImageButton android:id="@+id/stop_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:background="@null"
|
||||
android:src="@drawable/ic_media_stop" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -28,11 +28,13 @@
|
||||
|
||||
<string name="library_tab_text">Library</string>
|
||||
<string name="playlist_tab_text">Playlist</string>
|
||||
<string name="statistics_tab_text">Statistics</string>
|
||||
<string name="info_tab_text">Route Info</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_route_name">Variable 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 from Support7Demos</string>
|
||||
|
||||
<!-- GridLayout -->
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Presentation;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v7.media.MediaRouter.RouteInfo;
|
||||
import android.support.v7.media.MediaItemStatus;
|
||||
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.supportv7.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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getSnapshot() {
|
||||
return mOverlay.getSnapshot();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.media.MediaPlayer;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* MediaPlayerWrapper handles playback of a single media item, and is used for
|
||||
* both local and remote playback.
|
||||
*/
|
||||
public class MediaPlayerWrapper implements
|
||||
MediaPlayer.OnPreparedListener,
|
||||
MediaPlayer.OnCompletionListener,
|
||||
MediaPlayer.OnErrorListener,
|
||||
MediaPlayer.OnSeekCompleteListener,
|
||||
MediaSessionManager.Callback {
|
||||
private static final String TAG = "MediaPlayerWrapper";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
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 Callback mCallback;
|
||||
private Surface mSurface;
|
||||
private SurfaceHolder mSurfaceHolder;
|
||||
private int mSeekToPos;
|
||||
|
||||
public MediaPlayerWrapper(Context context) {
|
||||
mContext = context;
|
||||
reset();
|
||||
}
|
||||
|
||||
public void release() {
|
||||
onStop();
|
||||
mMediaPlayer.release();
|
||||
}
|
||||
|
||||
public void setCallback(Callback cb) {
|
||||
mCallback = cb;
|
||||
}
|
||||
|
||||
// MediaSessionManager.Callback
|
||||
@Override
|
||||
public void onNewItem(Uri uri) {
|
||||
reset();
|
||||
try {
|
||||
mMediaPlayer.setDataSource(mContext, uri);
|
||||
mMediaPlayer.prepareAsync();
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(TAG, "MediaPlayer throws IllegalStateException, uri=" + uri);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "MediaPlayer throws IOException, uri=" + uri);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "MediaPlayer throws IllegalArgumentException, uri=" + uri);
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "MediaPlayer throws SecurityException, uri=" + uri);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (mState == STATE_READY || mState == STATE_PAUSED) {
|
||||
mMediaPlayer.start();
|
||||
mState = STATE_PLAYING;
|
||||
} else if (mState == STATE_IDLE){
|
||||
mState = STATE_PLAY_PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||
mMediaPlayer.stop();
|
||||
mState = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (mState == STATE_PLAYING) {
|
||||
mMediaPlayer.pause();
|
||||
mState = STATE_PAUSED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeek(long pos) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onSeek: pos=" + pos);
|
||||
}
|
||||
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||
mMediaPlayer.seekTo((int)pos);
|
||||
mSeekToPos = (int)pos;
|
||||
} else if (mState == STATE_IDLE || mState == STATE_PLAY_PENDING) {
|
||||
// Seek before onPrepared() arrives,
|
||||
// need to performed delayed seek in onPrepared()
|
||||
mSeekToPos = (int)pos;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGetStatus(MediaQueueItem item) {
|
||||
if (mState == STATE_PLAYING || mState == STATE_PAUSED) {
|
||||
// use mSeekToPos if we're currently seeking (mSeekToPos is reset
|
||||
// when seeking is completed)
|
||||
item.setContentDuration(mMediaPlayer.getDuration());
|
||||
item.setContentPosition(mSeekToPos > 0 ?
|
||||
mSeekToPos : mMediaPlayer.getCurrentPosition());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSurface(Surface surface) {
|
||||
mSurface = surface;
|
||||
mSurfaceHolder = null;
|
||||
updateSurface();
|
||||
}
|
||||
|
||||
public void setSurface(SurfaceHolder surfaceHolder) {
|
||||
mSurface = null;
|
||||
mSurfaceHolder = surfaceHolder;
|
||||
updateSurface();
|
||||
}
|
||||
|
||||
//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) {
|
||||
Log.d(TAG, "Seeking to initial pos " + mSeekToPos);
|
||||
mMediaPlayer.seekTo((int)mSeekToPos);
|
||||
}
|
||||
mMediaPlayer.start();
|
||||
}
|
||||
if (mCallback != null) {
|
||||
mCallback.onStatusChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@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.onStatusChanged();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public 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 videoWidth = mMediaPlayer.getVideoWidth();
|
||||
int videoHeight = mMediaPlayer.getVideoHeight();
|
||||
if (videoWidth > 0 && videoHeight > 0) {
|
||||
if (mCallback != null) {
|
||||
mCallback.onSizeChanged(videoWidth, videoHeight);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "video rect is 0x0!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSurface() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Callback {
|
||||
public void onError() {}
|
||||
public void onCompletion() {}
|
||||
public void onStatusChanged() {}
|
||||
public void onSizeChanged(int width, int height) {}
|
||||
}
|
||||
|
||||
private static final class ICSMediaPlayer {
|
||||
public static final void setSurface(MediaPlayer player, Surface surface) {
|
||||
player.setSurface(surface);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import android.util.Log;
|
||||
import android.net.Uri;
|
||||
import android.app.PendingIntent;
|
||||
import android.support.v7.media.MediaItemStatus;
|
||||
|
||||
/**
|
||||
* MediaSessionManager 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 set of
|
||||
* callbacks MediaSessionManager.Callback, and is handled outside this class.
|
||||
*/
|
||||
public class MediaSessionManager {
|
||||
private static final String TAG = "MediaSessionManager";
|
||||
|
||||
private String mSessionId;
|
||||
private String mItemId;
|
||||
private String mCurItemId;
|
||||
private boolean mIsPlaying = true;
|
||||
private Callback mCallback;
|
||||
private List<MediaQueueItem> mQueue = new ArrayList<MediaQueueItem>();
|
||||
|
||||
public MediaSessionManager() {
|
||||
}
|
||||
|
||||
// Queue item (this maps to the ENQUEUE in the API which queues the item)
|
||||
public MediaQueueItem enqueue(String sid, Uri uri, PendingIntent receiver) {
|
||||
// fail if queue id is invalid
|
||||
if (sid != null && !sid.equals(mSessionId)) {
|
||||
Log.d(TAG, "invalid session id, mSessionId="+mSessionId+", sid="+sid);
|
||||
return null;
|
||||
}
|
||||
|
||||
// if queue id is unspecified, invalidate current queue
|
||||
if (sid == null) {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
mQueue.add(new MediaQueueItem(mSessionId, mItemId, uri, receiver));
|
||||
|
||||
if (updatePlaybackState()) {
|
||||
MediaQueueItem item = findItem(mItemId);
|
||||
mItemId = inc(mItemId);
|
||||
if (item == null) {
|
||||
Log.d(TAG, "item not found after it's added");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
removeItem(mItemId, MediaItemStatus.PLAYBACK_STATE_ERROR);
|
||||
return null;
|
||||
}
|
||||
|
||||
public MediaQueueItem remove(String sid, String iid) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return null;
|
||||
}
|
||||
return removeItem(iid, MediaItemStatus.PLAYBACK_STATE_CANCELED);
|
||||
}
|
||||
|
||||
// handles ERROR / COMPLETION
|
||||
public MediaQueueItem finish(boolean error) {
|
||||
return removeItem(mCurItemId, error ? MediaItemStatus.PLAYBACK_STATE_ERROR :
|
||||
MediaItemStatus.PLAYBACK_STATE_FINISHED);
|
||||
}
|
||||
|
||||
public MediaQueueItem seek(String sid, String iid, long pos) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < mQueue.size(); i++) {
|
||||
MediaQueueItem item = mQueue.get(i);
|
||||
if (iid.equals(item.getItemId())) {
|
||||
if (pos != item.getContentPosition()) {
|
||||
item.setContentPosition(pos);
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||
if (mCallback != null) {
|
||||
mCallback.onSeek(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public MediaQueueItem getCurrentItem() {
|
||||
return getStatus(mSessionId, mCurItemId);
|
||||
}
|
||||
|
||||
public MediaQueueItem getStatus(String sid, String iid) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < mQueue.size(); i++) {
|
||||
MediaQueueItem item = mQueue.get(i);
|
||||
if (iid.equals(item.getItemId())) {
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||
if (mCallback != null) {
|
||||
mCallback.onGetStatus(item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean pause(String sid) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return false;
|
||||
}
|
||||
mIsPlaying = false;
|
||||
return updatePlaybackState();
|
||||
}
|
||||
|
||||
public boolean resume(String sid) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return false;
|
||||
}
|
||||
mIsPlaying = true;
|
||||
return updatePlaybackState();
|
||||
}
|
||||
|
||||
public boolean stop(String sid) {
|
||||
if (sid == null || !sid.equals(mSessionId)) {
|
||||
return false;
|
||||
}
|
||||
clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setCallback(Callback cb) {
|
||||
mCallback = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String result = "Media Queue: ";
|
||||
if (!mQueue.isEmpty()) {
|
||||
for (MediaQueueItem item : mQueue) {
|
||||
result += "\n" + item.toString();
|
||||
}
|
||||
} else {
|
||||
result += "<empty>";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String inc(String id) {
|
||||
return (id == null) ? "0" : Integer.toString(Integer.parseInt(id)+1);
|
||||
}
|
||||
|
||||
// play the item at queue head
|
||||
private void play() {
|
||||
MediaQueueItem item = mQueue.get(0);
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||
mCurItemId = item.getItemId();
|
||||
if (mCallback != null) {
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
|
||||
mCallback.onNewItem(item.getUri());
|
||||
}
|
||||
mCallback.onStart();
|
||||
}
|
||||
item.setState(MediaItemStatus.PLAYBACK_STATE_PLAYING);
|
||||
}
|
||||
}
|
||||
|
||||
// stop the currently playing item
|
||||
private void stop() {
|
||||
MediaQueueItem item = mQueue.get(0);
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||
if (mCallback != null) {
|
||||
mCallback.onStop();
|
||||
}
|
||||
item.setState(MediaItemStatus.PLAYBACK_STATE_FINISHED);
|
||||
}
|
||||
}
|
||||
|
||||
// pause the currently playing item
|
||||
private void pause() {
|
||||
MediaQueueItem item = mQueue.get(0);
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
|
||||
if (mCallback != null) {
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PENDING) {
|
||||
mCallback.onNewItem(item.getUri());
|
||||
} else if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING) {
|
||||
mCallback.onPause();
|
||||
}
|
||||
}
|
||||
item.setState(MediaItemStatus.PLAYBACK_STATE_PAUSED);
|
||||
}
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
if (mQueue.size() > 0) {
|
||||
stop();
|
||||
mQueue.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
clear();
|
||||
mSessionId = inc(mSessionId);
|
||||
mItemId = "0";
|
||||
mIsPlaying = true;
|
||||
}
|
||||
|
||||
private boolean updatePlaybackState() {
|
||||
if (mQueue.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mIsPlaying) {
|
||||
play();
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private MediaQueueItem findItem(String iid) {
|
||||
for (MediaQueueItem item : mQueue) {
|
||||
if (iid.equals(item.getItemId())) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private MediaQueueItem removeItem(String iid, int state) {
|
||||
List<MediaQueueItem> queue =
|
||||
new ArrayList<MediaQueueItem>(mQueue.size());
|
||||
MediaQueueItem found = null;
|
||||
for (MediaQueueItem item : mQueue) {
|
||||
if (iid.equals(item.getItemId())) {
|
||||
if (item.getState() == MediaItemStatus.PLAYBACK_STATE_PLAYING
|
||||
|| item.getState() == MediaItemStatus.PLAYBACK_STATE_PAUSED) {
|
||||
stop();
|
||||
}
|
||||
item.setState(state);
|
||||
found = item;
|
||||
} else {
|
||||
queue.add(item);
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
mQueue = queue;
|
||||
updatePlaybackState();
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
public void onStart();
|
||||
public void onPause();
|
||||
public void onStop();
|
||||
public void onSeek(long pos);
|
||||
public void onGetStatus(MediaQueueItem item);
|
||||
public void onNewItem(Uri uri);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ package com.example.android.supportv7.media;
|
||||
import com.example.android.supportv7.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.display.DisplayManager;
|
||||
@@ -90,6 +91,8 @@ public abstract class OverlayDisplayWindow {
|
||||
|
||||
public abstract void updateAspectRatio(int width, int height);
|
||||
|
||||
public abstract Bitmap getSnapshot();
|
||||
|
||||
// Watches for significant changes in the overlay display window lifecycle.
|
||||
public interface OverlayWindowListener {
|
||||
public void onWindowCreated(Surface surface);
|
||||
@@ -164,6 +167,11 @@ public abstract class OverlayDisplayWindow {
|
||||
@Override
|
||||
public void updateAspectRatio(int width, int height) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getSnapshot() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,6 +258,11 @@ public abstract class OverlayDisplayWindow {
|
||||
relayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getSnapshot() {
|
||||
return mTextureView.getBitmap();
|
||||
}
|
||||
|
||||
private void relayout() {
|
||||
if (mWindowVisible) {
|
||||
updateWindowParams();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
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);
|
||||
|
||||
// track info for current media item
|
||||
public void updateTrackInfo() {}
|
||||
public String getDescription() { return ""; }
|
||||
public Bitmap getSnapshot() { return null; }
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@@ -16,40 +16,54 @@
|
||||
|
||||
package com.example.android.supportv7.media;
|
||||
|
||||
import android.support.v7.media.MediaItemStatus;
|
||||
import android.net.Uri;
|
||||
import android.app.PendingIntent;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v7.media.MediaItemStatus;
|
||||
|
||||
/**
|
||||
* MediaQueueItem helps keep track of the current status of an media item.
|
||||
* PlaylistItem helps keep track of the current status of an media item.
|
||||
*/
|
||||
final class MediaQueueItem {
|
||||
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 MediaQueueItem(String qid, String iid, Uri uri, PendingIntent pi) {
|
||||
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 setContentPosition(long pos) {
|
||||
public void setPosition(long pos) {
|
||||
mContentPosition = pos;
|
||||
}
|
||||
|
||||
public void setContentDuration(long duration) {
|
||||
public void setTimestamp(long ts) {
|
||||
mTimestamp = ts;
|
||||
}
|
||||
|
||||
public void setDuration(long duration) {
|
||||
mContentDuration = duration;
|
||||
}
|
||||
|
||||
@@ -61,6 +75,10 @@ final class MediaQueueItem {
|
||||
return mItemId;
|
||||
}
|
||||
|
||||
public String getRemoteItemId() {
|
||||
return mRemoteItemId;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return mUri;
|
||||
}
|
||||
@@ -73,18 +91,23 @@ final class MediaQueueItem {
|
||||
return mPlaybackState;
|
||||
}
|
||||
|
||||
public long getContentPosition() {
|
||||
public long getPosition() {
|
||||
return mContentPosition;
|
||||
}
|
||||
|
||||
public long getContentDuration() {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -101,6 +124,7 @@ final class MediaQueueItem {
|
||||
"ERROR"
|
||||
};
|
||||
return "[" + mSessionId + "|" + mItemId + "|"
|
||||
+ (mRemoteItemId != null ? mRemoteItemId : "-") + "|"
|
||||
+ state[mPlaybackState] + "] " + mUri.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.media.MediaItemStatus;
|
||||
import android.support.v7.media.MediaControlIntent;
|
||||
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 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 mTrackInfo = "";
|
||||
private Bitmap mSnapshot;
|
||||
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 updateTrackInfo() {
|
||||
// clear stats info first
|
||||
mTrackInfo = "";
|
||||
mSnapshot = null;
|
||||
|
||||
Intent intent = new Intent(SampleMediaRouteProvider.ACTION_GET_TRACK_INFO);
|
||||
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) {
|
||||
mTrackInfo = data.getString(SampleMediaRouteProvider.TRACK_INFO_DESC);
|
||||
mSnapshot = data.getParcelable(
|
||||
SampleMediaRouteProvider.TRACK_INFO_SNAPSHOT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error, Bundle data) {
|
||||
Log.d(TAG, "getStatistics: failed: error=" + error + ", data=" + data);
|
||||
}
|
||||
};
|
||||
|
||||
mRoute.sendControlRequest(intent, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return mTrackInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bitmap getSnapshot() {
|
||||
return mSnapshot;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.supportv7.media;
|
||||
|
||||
import com.example.android.supportv7.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.MediaRouteControllerDialog;
|
||||
import android.support.v7.media.MediaRouteSelector;
|
||||
import android.support.v7.media.MediaRouter;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* This class serves as an example on how to customize the media router control
|
||||
* dialog. It is derived from the standard MediaRouteControllerDialog with the
|
||||
* following overrides:
|
||||
*
|
||||
* 1. Shows thumbnail/snapshot of the current item
|
||||
*
|
||||
* 2. For variable volume routes, only allow volume control via Volume Up/Down
|
||||
* keys (to prevent accidental tapping on the volume adjust seekbar that sets
|
||||
* volume to maximum)
|
||||
*
|
||||
* 3. Provides transport control buttons (play/pause, stop)
|
||||
*/
|
||||
public class SampleMediaRouteControllerDialog extends MediaRouteControllerDialog {
|
||||
private static final String TAG = "SampleMediaRouteControllerDialog";
|
||||
private final SampleMediaRouterActivity mActivity;
|
||||
private final SessionManager mSessionManager;
|
||||
private final Player mPlayer;
|
||||
private ImageButton mPauseResumeButton;
|
||||
private ImageButton mStopButton;
|
||||
private ImageView mThumbnail;
|
||||
private TextView mTextView;
|
||||
private LinearLayout mInfoLayout;
|
||||
private LinearLayout mVolumeLayout;
|
||||
|
||||
public SampleMediaRouteControllerDialog(Context context,
|
||||
SessionManager manager, Player player) {
|
||||
super(context);
|
||||
mActivity = (SampleMediaRouterActivity) context;
|
||||
mSessionManager = manager;
|
||||
mPlayer = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateMediaControlView(Bundle savedInstanceState) {
|
||||
// Thumbnail and Track info
|
||||
View v = getLayoutInflater().inflate(R.layout.sample_media_controller, null);
|
||||
mInfoLayout = (LinearLayout)v.findViewById(R.id.media_route_info);
|
||||
mTextView = (TextView)v.findViewById(R.id.track_info);
|
||||
mThumbnail = (ImageView)v.findViewById(R.id.snapshot);
|
||||
|
||||
// Transport controls
|
||||
mPauseResumeButton = (ImageButton)v.findViewById(R.id.pause_resume_button);
|
||||
mPauseResumeButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mActivity != null) {
|
||||
mActivity.handleMediaKey(new KeyEvent(KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
mStopButton = (ImageButton)v.findViewById(R.id.stop_button);
|
||||
mStopButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mActivity != null) {
|
||||
mActivity.handleMediaKey(new KeyEvent(KeyEvent.ACTION_DOWN,
|
||||
KeyEvent.KEYCODE_MEDIA_STOP));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// update session status (will callback to updateUi at the end)
|
||||
mSessionManager.updateStatus();
|
||||
return v;
|
||||
}
|
||||
|
||||
public void updateUi() {
|
||||
String trackInfo = mPlayer.getDescription();
|
||||
Bitmap snapshot = mPlayer.getSnapshot();
|
||||
if (mPlayer.isRemotePlayback() && !trackInfo.isEmpty() && snapshot != null) {
|
||||
mInfoLayout.setVisibility(View.VISIBLE);
|
||||
mThumbnail.setImageBitmap(snapshot);
|
||||
mTextView.setText(trackInfo);
|
||||
} else {
|
||||
mInfoLayout.setVisibility(View.GONE);
|
||||
}
|
||||
// show pause or resume icon depending on current state
|
||||
mPauseResumeButton.setImageResource(mSessionManager.isPaused() ?
|
||||
R.drawable.ic_media_play : R.drawable.ic_media_pause);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.IntentFilter.MalformedMimeTypeException;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaRouter;
|
||||
import android.net.Uri;
|
||||
@@ -34,6 +35,7 @@ import android.support.v7.media.MediaRouteProvider;
|
||||
import android.support.v7.media.MediaRouter.ControlRequestCallback;
|
||||
import android.support.v7.media.MediaRouteProviderDescriptor;
|
||||
import android.support.v7.media.MediaRouteDescriptor;
|
||||
import android.support.v7.media.MediaSessionStatus;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Surface;
|
||||
@@ -50,7 +52,9 @@ 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_ROUTE_ID = "variable";
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -63,36 +67,36 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
/**
|
||||
* 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
|
||||
* @see #TRACK_INFO_DESC
|
||||
* @see #TRACK_INFO_SNAPSHOT
|
||||
*/
|
||||
public static final String ACTION_GET_STATISTICS =
|
||||
"com.example.android.supportv7.media.ACTION_GET_STATISTICS";
|
||||
public static final String ACTION_GET_TRACK_INFO =
|
||||
"com.example.android.supportv7.media.ACTION_GET_TRACK_INFO";
|
||||
|
||||
/**
|
||||
* {@link #ACTION_GET_STATISTICS} result data: Number of times the
|
||||
* playback action was invoked.
|
||||
* {@link #ACTION_GET_TRACK_INFO} result data: a string of information about
|
||||
* the currently playing media item
|
||||
*/
|
||||
public static final String DATA_PLAYBACK_COUNT =
|
||||
"com.example.android.supportv7.media.EXTRA_PLAYBACK_COUNT";
|
||||
public static final String TRACK_INFO_DESC =
|
||||
"com.example.android.supportv7.media.EXTRA_TRACK_INFO_DESC";
|
||||
|
||||
/*
|
||||
* Set ENABLE_QUEUEING to true to test queuing on MRP. This will make
|
||||
* MRP expose the following two experimental hidden APIs:
|
||||
* ACTION_ENQUEUE
|
||||
* ACTION_REMOVE
|
||||
/**
|
||||
* {@link #ACTION_GET_TRACK_INFO} result data: a bitmap containing a snapshot
|
||||
* of the currently playing media item
|
||||
*/
|
||||
public static final boolean ENABLE_QUEUEING = false;
|
||||
public static final String TRACK_INFO_SNAPSHOT =
|
||||
"com.example.android.supportv7.media.EXTRA_TRACK_INFO_SNAPSHOT";
|
||||
|
||||
private static final ArrayList<IntentFilter> CONTROL_FILTERS_BASIC;
|
||||
private static final ArrayList<IntentFilter> CONTROL_FILTERS_QUEUING;
|
||||
private static final ArrayList<IntentFilter> CONTROL_FILTERS_SESSION;
|
||||
|
||||
private static final ArrayList<IntentFilter> CONTROL_FILTERS;
|
||||
static {
|
||||
IntentFilter f1 = new IntentFilter();
|
||||
f1.addCategory(CATEGORY_SAMPLE_ROUTE);
|
||||
f1.addAction(ACTION_GET_STATISTICS);
|
||||
f1.addAction(ACTION_GET_TRACK_INFO);
|
||||
|
||||
IntentFilter f2 = new IntentFilter();
|
||||
f2.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||
@@ -124,14 +128,25 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
f5.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
|
||||
f5.addAction(MediaControlIntent.ACTION_REMOVE);
|
||||
|
||||
CONTROL_FILTERS = new ArrayList<IntentFilter>();
|
||||
CONTROL_FILTERS.add(f1);
|
||||
CONTROL_FILTERS.add(f2);
|
||||
CONTROL_FILTERS.add(f3);
|
||||
if (ENABLE_QUEUEING) {
|
||||
CONTROL_FILTERS.add(f4);
|
||||
CONTROL_FILTERS.add(f5);
|
||||
}
|
||||
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) {
|
||||
@@ -163,7 +178,7 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
FIXED_VOLUME_ROUTE_ID,
|
||||
r.getString(R.string.fixed_volume_route_name))
|
||||
.setDescription(r.getString(R.string.sample_route_description))
|
||||
.addControlFilters(CONTROL_FILTERS)
|
||||
.addControlFilters(CONTROL_FILTERS_BASIC)
|
||||
.setPlaybackStream(AudioManager.STREAM_MUSIC)
|
||||
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
|
||||
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
|
||||
@@ -171,10 +186,34 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
.build();
|
||||
|
||||
MediaRouteDescriptor routeDescriptor2 = new MediaRouteDescriptor.Builder(
|
||||
VARIABLE_VOLUME_ROUTE_ID,
|
||||
r.getString(R.string.variable_volume_route_name))
|
||||
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)
|
||||
.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)
|
||||
@@ -186,70 +225,58 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
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 OverlayDisplayWindow mOverlay;
|
||||
private final MediaPlayerWrapper mMediaPlayer;
|
||||
private final MediaSessionManager mSessionManager;
|
||||
private final SessionManager mSessionManager = new SessionManager("mrp");
|
||||
private final Player mPlayer;
|
||||
private PendingIntent mSessionReceiver;
|
||||
|
||||
public SampleRouteController(String routeId) {
|
||||
mRouteId = routeId;
|
||||
mMediaPlayer = new MediaPlayerWrapper(getContext());
|
||||
mSessionManager = new MediaSessionManager();
|
||||
mSessionManager.setCallback(mMediaPlayer);
|
||||
|
||||
// Create an overlay display window (used for simulating the remote playback only)
|
||||
mOverlay = OverlayDisplayWindow.create(getContext(),
|
||||
getContext().getResources().getString(
|
||||
R.string.sample_media_route_provider_remote),
|
||||
1024, 768, Gravity.CENTER);
|
||||
mOverlay.setOverlayWindowListener(new OverlayDisplayWindow.OverlayWindowListener() {
|
||||
mPlayer = Player.create(getContext(), null);
|
||||
mSessionManager.setPlayer(mPlayer);
|
||||
mSessionManager.setCallback(new SessionManager.Callback() {
|
||||
@Override
|
||||
public void onWindowCreated(Surface surface) {
|
||||
mMediaPlayer.setSurface(surface);
|
||||
public void onStatusChanged() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowCreated(SurfaceHolder surfaceHolder) {
|
||||
mMediaPlayer.setSurface(surfaceHolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowDestroyed() {
|
||||
public void onItemChanged(PlaylistItem item) {
|
||||
handleStatusChange(item);
|
||||
}
|
||||
});
|
||||
|
||||
mMediaPlayer.setCallback(new MediaPlayerCallback());
|
||||
setVolumeInternal(mVolume);
|
||||
Log.d(TAG, mRouteId + ": Controller created");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
Log.d(TAG, mRouteId + ": Controller released");
|
||||
mMediaPlayer.release();
|
||||
mPlayer.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelect() {
|
||||
Log.d(TAG, mRouteId + ": Selected");
|
||||
mOverlay.show();
|
||||
mPlayer.connect(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnselect() {
|
||||
Log.d(TAG, mRouteId + ": Unselected");
|
||||
mMediaPlayer.onStop();
|
||||
mOverlay.dismiss();
|
||||
mPlayer.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetVolume(int volume) {
|
||||
Log.d(TAG, mRouteId + ": Set volume to " + volume);
|
||||
if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
|
||||
if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
|
||||
setVolumeInternal(volume);
|
||||
}
|
||||
}
|
||||
@@ -257,7 +284,7 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
@Override
|
||||
public void onUpdateVolume(int delta) {
|
||||
Log.d(TAG, mRouteId + ": Update volume by " + delta);
|
||||
if (mRouteId.equals(VARIABLE_VOLUME_ROUTE_ID)) {
|
||||
if (!mRouteId.equals(FIXED_VOLUME_ROUTE_ID)) {
|
||||
setVolumeInternal(mVolume + delta);
|
||||
}
|
||||
}
|
||||
@@ -284,15 +311,25 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
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)
|
||||
if (action.equals(ACTION_GET_TRACK_INFO)
|
||||
&& intent.hasCategory(CATEGORY_SAMPLE_ROUTE)) {
|
||||
Bundle data = new Bundle();
|
||||
data.putInt(DATA_PLAYBACK_COUNT, mEnqueueCount);
|
||||
PlaylistItem item = mSessionManager.getCurrentItem();
|
||||
if (item != null) {
|
||||
data.putString(TRACK_INFO_DESC, item.toString());
|
||||
data.putParcelable(TRACK_INFO_SNAPSHOT, mPlayer.getSnapshot());
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.onResult(data);
|
||||
}
|
||||
@@ -314,23 +351,31 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
private boolean handlePlay(Intent intent, ControlRequestCallback callback) {
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
if (sid == null || mSessionManager.stop(sid)) {
|
||||
Log.d(TAG, "handleEnqueue");
|
||||
return handleEnqueue(intent, callback);
|
||||
if (sid != null && !sid.equals(mSessionManager.getSessionId())) {
|
||||
Log.d(TAG, "handlePlay fails because of bad sid="+sid);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
if (mSessionManager.hasSession()) {
|
||||
mSessionManager.stop();
|
||||
}
|
||||
return handleEnqueue(intent, callback);
|
||||
}
|
||||
|
||||
private boolean handleEnqueue(Intent intent, ControlRequestCallback callback) {
|
||||
if (intent.getData() == null) {
|
||||
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;
|
||||
}
|
||||
|
||||
mEnqueueCount +=1;
|
||||
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);
|
||||
Uri uri = intent.getData();
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
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);
|
||||
@@ -339,12 +384,13 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
Log.d(TAG, mRouteId + ": Received " + (enqueue?"enqueue":"play") + " request"
|
||||
+ ", uri=" + uri
|
||||
+ ", mime=" + mime
|
||||
+ ", sid=" + sid
|
||||
+ ", pos=" + pos
|
||||
+ ", metadata=" + metadata
|
||||
+ ", headers=" + headers
|
||||
+ ", receiver=" + receiver);
|
||||
MediaQueueItem item = mSessionManager.enqueue(sid, uri, receiver);
|
||||
PlaylistItem item = mSessionManager.add(uri, mime, receiver);
|
||||
if (callback != null) {
|
||||
if (item != null) {
|
||||
Bundle result = new Bundle();
|
||||
@@ -357,13 +403,18 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
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);
|
||||
MediaQueueItem item = mSessionManager.remove(sid, iid);
|
||||
PlaylistItem item = mSessionManager.remove(iid);
|
||||
if (callback != null) {
|
||||
if (item != null) {
|
||||
Bundle result = new Bundle();
|
||||
@@ -380,10 +431,14 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
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);
|
||||
MediaQueueItem item = mSessionManager.seek(sid, iid, pos);
|
||||
PlaylistItem item = mSessionManager.seek(iid, pos);
|
||||
if (callback != null) {
|
||||
if (item != null) {
|
||||
Bundle result = new Bundle();
|
||||
@@ -401,7 +456,8 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
private boolean handleGetStatus(Intent intent, ControlRequestCallback callback) {
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
String iid = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
|
||||
MediaQueueItem item = mSessionManager.getStatus(sid, iid);
|
||||
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();
|
||||
@@ -418,10 +474,12 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
private boolean handlePause(Intent intent, ControlRequestCallback callback) {
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
boolean success = mSessionManager.pause(sid);
|
||||
boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
|
||||
mSessionManager.pause();
|
||||
if (callback != null) {
|
||||
if (success) {
|
||||
callback.onResult(null);
|
||||
callback.onResult(new Bundle());
|
||||
handleSessionStatusChange(sid);
|
||||
} else {
|
||||
callback.onError("Failed to pause, sid=" + sid, null);
|
||||
}
|
||||
@@ -431,10 +489,12 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
private boolean handleResume(Intent intent, ControlRequestCallback callback) {
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
boolean success = mSessionManager.resume(sid);
|
||||
boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
|
||||
mSessionManager.resume();
|
||||
if (callback != null) {
|
||||
if (success) {
|
||||
callback.onResult(null);
|
||||
callback.onResult(new Bundle());
|
||||
handleSessionStatusChange(sid);
|
||||
} else {
|
||||
callback.onError("Failed to resume, sid=" + sid, null);
|
||||
}
|
||||
@@ -444,10 +504,12 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
|
||||
private boolean handleStop(Intent intent, ControlRequestCallback callback) {
|
||||
String sid = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
|
||||
boolean success = mSessionManager.stop(sid);
|
||||
boolean success = (sid != null) && sid.equals(mSessionManager.getSessionId());
|
||||
mSessionManager.stop();
|
||||
if (callback != null) {
|
||||
if (success) {
|
||||
callback.onResult(null);
|
||||
callback.onResult(new Bundle());
|
||||
handleSessionStatusChange(sid);
|
||||
} else {
|
||||
callback.onError("Failed to stop, sid=" + sid, null);
|
||||
}
|
||||
@@ -455,14 +517,64 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
return success;
|
||||
}
|
||||
|
||||
private void handleFinish(boolean error) {
|
||||
MediaQueueItem item = mSessionManager.finish(error);
|
||||
if (item != null) {
|
||||
handleStatusChange(item);
|
||||
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 void handleStatusChange(MediaQueueItem item) {
|
||||
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();
|
||||
}
|
||||
@@ -484,25 +596,18 @@ final class SampleMediaRouteProvider extends MediaRouteProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private final class MediaPlayerCallback extends MediaPlayerWrapper.Callback {
|
||||
@Override
|
||||
public void onError() {
|
||||
handleFinish(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion() {
|
||||
handleFinish(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged() {
|
||||
handleStatusChange(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSizeChanged(int width, int height) {
|
||||
mOverlay.updateAspectRatio(width, height);
|
||||
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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* 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.supportv7.media;
|
||||
|
||||
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 java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 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 Player.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 isPaused() {
|
||||
return hasSession() && mPaused;
|
||||
}
|
||||
|
||||
public boolean hasSession() {
|
||||
return mSessionValid;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return mSessionValid ? Integer.toString(mSessionId) : null;
|
||||
}
|
||||
|
||||
public PlaylistItem getCurrentItem() {
|
||||
return mPlaylist.isEmpty() ? null : mPlaylist.get(0);
|
||||
}
|
||||
|
||||
// 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.updateTrackInfo();
|
||||
|
||||
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");
|
||||
}
|
||||
if (!mSessionValid) {
|
||||
return;
|
||||
}
|
||||
checkPlayer();
|
||||
mPaused = true;
|
||||
updatePlaybackState();
|
||||
}
|
||||
|
||||
public void resume() {
|
||||
if (DEBUG) {
|
||||
log("resume");
|
||||
}
|
||||
if (!mSessionValid) {
|
||||
return;
|
||||
}
|
||||
checkPlayer();
|
||||
mPaused = false;
|
||||
updatePlaybackState();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (DEBUG) {
|
||||
log("stop");
|
||||
}
|
||||
if (!mSessionValid) {
|
||||
return;
|
||||
}
|
||||
checkPlayer();
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
15
samples/Vault/Android.mk
Normal file
@@ -0,0 +1,15 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-subdir-java-files)
|
||||
|
||||
LOCAL_SDK_VERSION := current
|
||||
|
||||
LOCAL_PROGUARD_ENABLED := disabled
|
||||
|
||||
LOCAL_PACKAGE_NAME := Vault
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
21
samples/Vault/AndroidManifest.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.vault">
|
||||
|
||||
<application
|
||||
android:label="@string/app_label"
|
||||
android:icon="@drawable/ic_lock_lock">
|
||||
<provider
|
||||
android:name=".VaultProvider"
|
||||
android:authorities="com.example.android.vault.provider"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS"
|
||||
android:enabled="@bool/isAtLeastKitKat">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
</intent-filter>
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
samples/Vault/res/drawable-xhdpi/ic_lock_lock.png
Normal file
|
After Width: | Height: | Size: 954 B |
4
samples/Vault/res/values-v19/bool.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="isAtLeastKitKat">true</bool>
|
||||
</resources>
|
||||
4
samples/Vault/res/values/bool.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<bool name="isAtLeastKitKat">false</bool>
|
||||
</resources>
|
||||
21
samples/Vault/res/values/strings.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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="app_label">Vault</string>
|
||||
<string name="info_software">Software-backed</string>
|
||||
<string name="info_software_detail">Encryption key is software-backed, which is less secure.</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
* 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.vault;
|
||||
|
||||
import static com.example.android.vault.VaultProvider.TAG;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.ProtocolException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.DigestException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
/**
|
||||
* Represents a single encrypted document stored on disk. Handles encryption,
|
||||
* decryption, and authentication of the document when requested.
|
||||
* <p>
|
||||
* Encrypted documents are stored on disk as a magic number, followed by an
|
||||
* encrypted metadata section, followed by an encrypted content section. The
|
||||
* content section always starts at a specific offset {@link #CONTENT_OFFSET} to
|
||||
* allow metadata updates without rewriting the entire file.
|
||||
* <p>
|
||||
* Each section is encrypted using AES-128 with a random IV, and authenticated
|
||||
* with SHA-256. Data encrypted and authenticated like this can be safely stored
|
||||
* on untrusted storage devices, as long as the keys are stored securely.
|
||||
* <p>
|
||||
* Not inherently thread safe.
|
||||
*/
|
||||
public class EncryptedDocument {
|
||||
|
||||
/**
|
||||
* Magic number to identify file; "AVLT".
|
||||
*/
|
||||
private static final int MAGIC_NUMBER = 0x41564c54;
|
||||
|
||||
/**
|
||||
* Offset in file at which content section starts. Magic and metadata
|
||||
* section must fully fit before this offset.
|
||||
*/
|
||||
private static final int CONTENT_OFFSET = 4096;
|
||||
|
||||
private static final boolean DEBUG_METADATA = true;
|
||||
|
||||
/** Key length for AES-128 */
|
||||
public static final int DATA_KEY_LENGTH = 16;
|
||||
/** Key length for SHA-256 */
|
||||
public static final int MAC_KEY_LENGTH = 32;
|
||||
|
||||
private final SecureRandom mRandom;
|
||||
private final Cipher mCipher;
|
||||
private final Mac mMac;
|
||||
|
||||
private final long mDocId;
|
||||
private final File mFile;
|
||||
private final SecretKey mDataKey;
|
||||
private final SecretKey mMacKey;
|
||||
|
||||
/**
|
||||
* Create an encrypted document.
|
||||
*
|
||||
* @param docId the expected {@link Document#COLUMN_DOCUMENT_ID} to be
|
||||
* validated when reading metadata.
|
||||
* @param file location on disk where the encrypted document is stored. May
|
||||
* not exist yet.
|
||||
*/
|
||||
public EncryptedDocument(long docId, File file, SecretKey dataKey, SecretKey macKey)
|
||||
throws GeneralSecurityException {
|
||||
mRandom = new SecureRandom();
|
||||
mCipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
mMac = Mac.getInstance("HmacSHA256");
|
||||
|
||||
if (dataKey.getEncoded().length != DATA_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("Expected data key length " + DATA_KEY_LENGTH);
|
||||
}
|
||||
if (macKey.getEncoded().length != MAC_KEY_LENGTH) {
|
||||
throw new IllegalArgumentException("Expected MAC key length " + MAC_KEY_LENGTH);
|
||||
}
|
||||
|
||||
mDocId = docId;
|
||||
mFile = file;
|
||||
mDataKey = dataKey;
|
||||
mMacKey = macKey;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return mFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return mFile.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and return parsed metadata section from this document.
|
||||
*
|
||||
* @throws DigestException if metadata fails MAC check, or if
|
||||
* {@link Document#COLUMN_DOCUMENT_ID} recorded in metadata is
|
||||
* unexpected.
|
||||
*/
|
||||
public JSONObject readMetadata() throws IOException, GeneralSecurityException {
|
||||
final RandomAccessFile f = new RandomAccessFile(mFile, "r");
|
||||
try {
|
||||
assertMagic(f);
|
||||
|
||||
// Only interested in metadata section
|
||||
final ByteArrayOutputStream metaOut = new ByteArrayOutputStream();
|
||||
readSection(f, metaOut);
|
||||
|
||||
final String rawMeta = metaOut.toString(StandardCharsets.UTF_8.name());
|
||||
if (DEBUG_METADATA) {
|
||||
Log.d(TAG, "Found metadata for " + mDocId + ": " + rawMeta);
|
||||
}
|
||||
|
||||
final JSONObject meta = new JSONObject(rawMeta);
|
||||
|
||||
// Validate that metadata belongs to requested file
|
||||
if (meta.getLong(Document.COLUMN_DOCUMENT_ID) != mDocId) {
|
||||
throw new DigestException("Unexpected document ID");
|
||||
}
|
||||
|
||||
return meta;
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt and read content section of this document, writing it into the
|
||||
* given pipe.
|
||||
* <p>
|
||||
* Pipe is left open, so caller is responsible for calling
|
||||
* {@link ParcelFileDescriptor#close()} or
|
||||
* {@link ParcelFileDescriptor#closeWithError(String)}.
|
||||
*
|
||||
* @param contentOut write end of a pipe.
|
||||
* @throws DigestException if content fails MAC check. Some or all content
|
||||
* may have already been written to the pipe when the MAC is
|
||||
* validated.
|
||||
*/
|
||||
public void readContent(ParcelFileDescriptor contentOut)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final RandomAccessFile f = new RandomAccessFile(mFile, "r");
|
||||
try {
|
||||
assertMagic(f);
|
||||
|
||||
if (f.length() <= CONTENT_OFFSET) {
|
||||
throw new IOException("Document has no content");
|
||||
}
|
||||
|
||||
// Skip over metadata section
|
||||
f.seek(CONTENT_OFFSET);
|
||||
readSection(f, new FileOutputStream(contentOut.getFileDescriptor()));
|
||||
|
||||
} finally {
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt and write both the metadata and content sections of this
|
||||
* document, reading the content from the given pipe. Internally uses
|
||||
* {@link ParcelFileDescriptor#checkError()} to verify that content arrives
|
||||
* without errors. Writes to temporary file to keep atomic view of contents,
|
||||
* swapping into place only when write is successful.
|
||||
* <p>
|
||||
* Pipe is left open, so caller is responsible for calling
|
||||
* {@link ParcelFileDescriptor#close()} or
|
||||
* {@link ParcelFileDescriptor#closeWithError(String)}.
|
||||
*
|
||||
* @param contentIn read end of a pipe.
|
||||
*/
|
||||
public void writeMetadataAndContent(JSONObject meta, ParcelFileDescriptor contentIn)
|
||||
throws IOException, GeneralSecurityException {
|
||||
// Write into temporary file to provide an atomic view of existing
|
||||
// contents during write, and also to recover from failed writes.
|
||||
final String tempName = mFile.getName() + ".tmp_" + Thread.currentThread().getId();
|
||||
final File tempFile = new File(mFile.getParentFile(), tempName);
|
||||
|
||||
RandomAccessFile f = new RandomAccessFile(tempFile, "rw");
|
||||
try {
|
||||
// Truncate any existing data
|
||||
f.setLength(0);
|
||||
|
||||
// Write content first to detect size
|
||||
if (contentIn != null) {
|
||||
f.seek(CONTENT_OFFSET);
|
||||
final int plainLength = writeSection(
|
||||
f, new FileInputStream(contentIn.getFileDescriptor()));
|
||||
meta.put(Document.COLUMN_SIZE, plainLength);
|
||||
|
||||
// Verify that remote side of pipe finished okay; if they
|
||||
// crashed or indicated an error then this throws and we
|
||||
// leave the original file intact and clean up temp below.
|
||||
contentIn.checkError();
|
||||
}
|
||||
|
||||
meta.put(Document.COLUMN_DOCUMENT_ID, mDocId);
|
||||
meta.put(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
|
||||
|
||||
// Rewind and write metadata section
|
||||
f.seek(0);
|
||||
f.writeInt(MAGIC_NUMBER);
|
||||
|
||||
final ByteArrayInputStream metaIn = new ByteArrayInputStream(
|
||||
meta.toString().getBytes(StandardCharsets.UTF_8));
|
||||
writeSection(f, metaIn);
|
||||
|
||||
if (f.getFilePointer() > CONTENT_OFFSET) {
|
||||
throw new IOException("Metadata section was too large");
|
||||
}
|
||||
|
||||
// Everything written fine, atomically swap new data into place.
|
||||
// fsync() before close would be overkill, since rename() is an
|
||||
// atomic barrier.
|
||||
f.close();
|
||||
tempFile.renameTo(mFile);
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
// Regardless of what happens, always try cleaning up.
|
||||
f.close();
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decrypt the section starting at the current file offset.
|
||||
* Validates MAC of decrypted data, throwing if mismatch. When finished,
|
||||
* file offset is at the end of the entire section.
|
||||
*/
|
||||
private void readSection(RandomAccessFile f, OutputStream out)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final long start = f.getFilePointer();
|
||||
|
||||
final Section section = new Section();
|
||||
section.read(f);
|
||||
|
||||
final IvParameterSpec ivSpec = new IvParameterSpec(section.iv);
|
||||
mCipher.init(Cipher.DECRYPT_MODE, mDataKey, ivSpec);
|
||||
mMac.init(mMacKey);
|
||||
|
||||
byte[] inbuf = new byte[8192];
|
||||
byte[] outbuf;
|
||||
int n;
|
||||
while ((n = f.read(inbuf, 0, (int) Math.min(section.length, inbuf.length))) != -1) {
|
||||
section.length -= n;
|
||||
mMac.update(inbuf, 0, n);
|
||||
outbuf = mCipher.update(inbuf, 0, n);
|
||||
if (outbuf != null) {
|
||||
out.write(outbuf);
|
||||
}
|
||||
if (section.length == 0) break;
|
||||
}
|
||||
|
||||
section.assertMac(mMac.doFinal());
|
||||
|
||||
outbuf = mCipher.doFinal();
|
||||
if (outbuf != null) {
|
||||
out.write(outbuf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt and write the given stream as a full section. Writes section
|
||||
* header and encrypted data starting at the current file offset. When
|
||||
* finished, file offset is at the end of the entire section.
|
||||
*/
|
||||
private int writeSection(RandomAccessFile f, InputStream in)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final long start = f.getFilePointer();
|
||||
|
||||
// Write header; we'll come back later to finalize details
|
||||
final Section section = new Section();
|
||||
section.write(f);
|
||||
|
||||
final long dataStart = f.getFilePointer();
|
||||
|
||||
mRandom.nextBytes(section.iv);
|
||||
|
||||
final IvParameterSpec ivSpec = new IvParameterSpec(section.iv);
|
||||
mCipher.init(Cipher.ENCRYPT_MODE, mDataKey, ivSpec);
|
||||
mMac.init(mMacKey);
|
||||
|
||||
int plainLength = 0;
|
||||
byte[] inbuf = new byte[8192];
|
||||
byte[] outbuf;
|
||||
int n;
|
||||
while ((n = in.read(inbuf)) != -1) {
|
||||
plainLength += n;
|
||||
outbuf = mCipher.update(inbuf, 0, n);
|
||||
if (outbuf != null) {
|
||||
mMac.update(outbuf);
|
||||
f.write(outbuf);
|
||||
}
|
||||
}
|
||||
|
||||
outbuf = mCipher.doFinal();
|
||||
if (outbuf != null) {
|
||||
mMac.update(outbuf);
|
||||
f.write(outbuf);
|
||||
}
|
||||
|
||||
section.setMac(mMac.doFinal());
|
||||
|
||||
final long dataEnd = f.getFilePointer();
|
||||
section.length = dataEnd - dataStart;
|
||||
|
||||
// Rewind and update header
|
||||
f.seek(start);
|
||||
section.write(f);
|
||||
f.seek(dataEnd);
|
||||
|
||||
return plainLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header of a single file section.
|
||||
*/
|
||||
private static class Section {
|
||||
long length;
|
||||
final byte[] iv = new byte[DATA_KEY_LENGTH];
|
||||
final byte[] mac = new byte[MAC_KEY_LENGTH];
|
||||
|
||||
public void read(RandomAccessFile f) throws IOException {
|
||||
length = f.readLong();
|
||||
f.readFully(iv);
|
||||
f.readFully(mac);
|
||||
}
|
||||
|
||||
public void write(RandomAccessFile f) throws IOException {
|
||||
f.writeLong(length);
|
||||
f.write(iv);
|
||||
f.write(mac);
|
||||
}
|
||||
|
||||
public void setMac(byte[] mac) {
|
||||
if (mac.length != this.mac.length) {
|
||||
throw new IllegalArgumentException("Unexpected MAC length");
|
||||
}
|
||||
System.arraycopy(mac, 0, this.mac, 0, this.mac.length);
|
||||
}
|
||||
|
||||
public void assertMac(byte[] mac) throws DigestException {
|
||||
if (mac.length != this.mac.length) {
|
||||
throw new IllegalArgumentException("Unexpected MAC length");
|
||||
}
|
||||
byte result = 0;
|
||||
for (int i = 0; i < mac.length; i++) {
|
||||
result |= mac[i] ^ this.mac[i];
|
||||
}
|
||||
if (result != 0) {
|
||||
throw new DigestException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertMagic(RandomAccessFile f) throws IOException {
|
||||
final int magic = f.readInt();
|
||||
if (magic != MAGIC_NUMBER) {
|
||||
throw new ProtocolException("Bad magic number: " + Integer.toHexString(magic));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.vault;
|
||||
|
||||
import android.content.Context;
|
||||
import android.security.KeyPairGeneratorSpec;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* Wraps {@link SecretKey} instances using a public/private key pair stored in
|
||||
* the platform {@link KeyStore}. This allows us to protect symmetric keys with
|
||||
* hardware-backed crypto, if provided by the device.
|
||||
* <p>
|
||||
* See <a href="http://en.wikipedia.org/wiki/Key_Wrap">key wrapping</a> for more
|
||||
* details.
|
||||
* <p>
|
||||
* Not inherently thread safe.
|
||||
*/
|
||||
public class SecretKeyWrapper {
|
||||
private final Cipher mCipher;
|
||||
private final KeyPair mPair;
|
||||
|
||||
/**
|
||||
* Create a wrapper using the public/private key pair with the given alias.
|
||||
* If no pair with that alias exists, it will be generated.
|
||||
*/
|
||||
public SecretKeyWrapper(Context context, String alias)
|
||||
throws GeneralSecurityException, IOException {
|
||||
mCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
|
||||
|
||||
final KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
|
||||
keyStore.load(null);
|
||||
|
||||
if (!keyStore.containsAlias(alias)) {
|
||||
generateKeyPair(context, alias);
|
||||
}
|
||||
|
||||
// Even if we just generated the key, always read it back to ensure we
|
||||
// can read it successfully.
|
||||
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(
|
||||
alias, null);
|
||||
mPair = new KeyPair(entry.getCertificate().getPublicKey(), entry.getPrivateKey());
|
||||
}
|
||||
|
||||
private static void generateKeyPair(Context context, String alias)
|
||||
throws GeneralSecurityException {
|
||||
final Calendar start = new GregorianCalendar();
|
||||
final Calendar end = new GregorianCalendar();
|
||||
end.add(Calendar.YEAR, 100);
|
||||
|
||||
final KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
|
||||
.setAlias(alias)
|
||||
.setSubject(new X500Principal("CN=" + alias))
|
||||
.setSerialNumber(BigInteger.ONE)
|
||||
.setStartDate(start.getTime())
|
||||
.setEndDate(end.getTime())
|
||||
.build();
|
||||
|
||||
final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
|
||||
gen.initialize(spec);
|
||||
gen.generateKeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a {@link SecretKey} using the public key assigned to this wrapper.
|
||||
* Use {@link #unwrap(byte[])} to later recover the original
|
||||
* {@link SecretKey}.
|
||||
*
|
||||
* @return a wrapped version of the given {@link SecretKey} that can be
|
||||
* safely stored on untrusted storage.
|
||||
*/
|
||||
public byte[] wrap(SecretKey key) throws GeneralSecurityException {
|
||||
mCipher.init(Cipher.WRAP_MODE, mPair.getPublic());
|
||||
return mCipher.wrap(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a {@link SecretKey} using the private key assigned to this
|
||||
* wrapper.
|
||||
*
|
||||
* @param blob a wrapped {@link SecretKey} as previously returned by
|
||||
* {@link #wrap(SecretKey)}.
|
||||
*/
|
||||
public SecretKey unwrap(byte[] blob) throws GeneralSecurityException {
|
||||
mCipher.init(Cipher.UNWRAP_MODE, mPair.getPrivate());
|
||||
return (SecretKey) mCipher.unwrap(blob, "AES", Cipher.SECRET_KEY);
|
||||
}
|
||||
}
|
||||
72
samples/Vault/src/com/example/android/vault/Utils.java
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (C) 2013 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.android.vault;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class Utils {
|
||||
public static void closeQuietly(Closeable closable) {
|
||||
if (closable != null) {
|
||||
try {
|
||||
closable.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void closeWithErrorQuietly(ParcelFileDescriptor pfd, String msg) {
|
||||
if (pfd != null) {
|
||||
try {
|
||||
pfd.closeWithError(msg);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeFully(File file, byte[] data) throws IOException {
|
||||
final OutputStream out = new FileOutputStream(file);
|
||||
try {
|
||||
out.write(data);
|
||||
} finally {
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] readFully(File file) throws IOException {
|
||||
final InputStream in = new FileInputStream(file);
|
||||
try {
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
bytes.write(buffer, 0, count);
|
||||
}
|
||||
return bytes.toByteArray();
|
||||
} finally {
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
563
samples/Vault/src/com/example/android/vault/VaultProvider.java
Normal file
@@ -0,0 +1,563 @@
|
||||
/*
|
||||
* 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.vault;
|
||||
|
||||
import static com.example.android.vault.EncryptedDocument.DATA_KEY_LENGTH;
|
||||
import static com.example.android.vault.EncryptedDocument.MAC_KEY_LENGTH;
|
||||
import static com.example.android.vault.Utils.closeQuietly;
|
||||
import static com.example.android.vault.Utils.closeWithErrorQuietly;
|
||||
import static com.example.android.vault.Utils.readFully;
|
||||
import static com.example.android.vault.Utils.writeFully;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MatrixCursor.RowBuilder;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.security.KeyChain;
|
||||
import android.util.Log;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Provider that encrypts both metadata and contents of documents stored inside.
|
||||
* Each document is stored as described by {@link EncryptedDocument} with
|
||||
* separate metadata and content sections. Directories are just
|
||||
* {@link EncryptedDocument} instances without a content section, and a list of
|
||||
* child documents included in the metadata section.
|
||||
* <p>
|
||||
* All content is encrypted/decrypted on demand through pipes, using
|
||||
* {@link ParcelFileDescriptor#createReliablePipe()} to detect and recover from
|
||||
* remote crashes and errors.
|
||||
* <p>
|
||||
* Our symmetric encryption key is stored on disk only after using
|
||||
* {@link SecretKeyWrapper} to "wrap" it using another public/private key pair
|
||||
* stored in the platform {@link KeyStore}. This allows us to protect our
|
||||
* symmetric key with hardware-backed keys, if supported. Devices without
|
||||
* hardware support still encrypt their keys while at rest, and the platform
|
||||
* always requires a user to present a PIN, password, or pattern to unlock the
|
||||
* KeyStore before use.
|
||||
*/
|
||||
public class VaultProvider extends DocumentsProvider {
|
||||
public static final String TAG = "Vault";
|
||||
|
||||
static final String AUTHORITY = "com.example.android.vault.provider";
|
||||
|
||||
static final String DEFAULT_ROOT_ID = "vault";
|
||||
static final String DEFAULT_DOCUMENT_ID = "0";
|
||||
|
||||
/** JSON key storing array of all children documents in a directory. */
|
||||
private static final String KEY_CHILDREN = "vault:children";
|
||||
|
||||
/** Key pointing to next available document ID. */
|
||||
private static final String PREF_NEXT_ID = "next_id";
|
||||
|
||||
/** Blob used to derive {@link #mDataKey} from our secret key. */
|
||||
private static final byte[] BLOB_DATA = "DATA".getBytes(StandardCharsets.UTF_8);
|
||||
/** Blob used to derive {@link #mMacKey} from our secret key. */
|
||||
private static final byte[] BLOB_MAC = "MAC".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
|
||||
Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_SUMMARY
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
|
||||
};
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String[] resolveDocumentProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
private final Object mIdLock = new Object();
|
||||
|
||||
/**
|
||||
* Flag indicating that the {@link SecretKeyWrapper} public/private key is
|
||||
* hardware-backed. A software keystore is more vulnerable to offline
|
||||
* attacks if the device is compromised.
|
||||
*/
|
||||
private boolean mHardwareBacked;
|
||||
|
||||
/** File where wrapped symmetric key is stored. */
|
||||
private File mKeyFile;
|
||||
/** Directory where all encrypted documents are stored. */
|
||||
private File mDocumentsDir;
|
||||
|
||||
private SecretKey mDataKey;
|
||||
private SecretKey mMacKey;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
mHardwareBacked = KeyChain.isBoundKeyAlgorithm("RSA");
|
||||
|
||||
mKeyFile = new File(getContext().getFilesDir(), "vault.key");
|
||||
mDocumentsDir = new File(getContext().getFilesDir(), "documents");
|
||||
mDocumentsDir.mkdirs();
|
||||
|
||||
try {
|
||||
// Load secret key and ensure our root document is ready.
|
||||
loadOrGenerateKeys(getContext(), mKeyFile);
|
||||
initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for testing.
|
||||
*/
|
||||
void wipeAllContents() throws IOException, GeneralSecurityException {
|
||||
for (File f : mDocumentsDir.listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
|
||||
initDocument(Long.parseLong(DEFAULT_DOCUMENT_ID), Document.MIME_TYPE_DIR, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load our symmetric secret key and use it to derive two different data and
|
||||
* MAC keys. The symmetric secret key is stored securely on disk by wrapping
|
||||
* it with a public/private key pair, possibly backed by hardware.
|
||||
*/
|
||||
private void loadOrGenerateKeys(Context context, File keyFile)
|
||||
throws GeneralSecurityException, IOException {
|
||||
final SecretKeyWrapper wrapper = new SecretKeyWrapper(context, TAG);
|
||||
|
||||
// Generate secret key if none exists
|
||||
if (!keyFile.exists()) {
|
||||
final byte[] raw = new byte[DATA_KEY_LENGTH];
|
||||
new SecureRandom().nextBytes(raw);
|
||||
|
||||
final SecretKey key = new SecretKeySpec(raw, "AES");
|
||||
final byte[] wrapped = wrapper.wrap(key);
|
||||
|
||||
writeFully(keyFile, wrapped);
|
||||
}
|
||||
|
||||
// Even if we just generated the key, always read it back to ensure we
|
||||
// can read it successfully.
|
||||
final byte[] wrapped = readFully(keyFile);
|
||||
final SecretKey key = wrapper.unwrap(wrapped);
|
||||
|
||||
final Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
|
||||
// Derive two different keys for encryption and authentication.
|
||||
final byte[] rawDataKey = new byte[DATA_KEY_LENGTH];
|
||||
final byte[] rawMacKey = new byte[MAC_KEY_LENGTH];
|
||||
|
||||
System.arraycopy(mac.doFinal(BLOB_DATA), 0, rawDataKey, 0, rawDataKey.length);
|
||||
System.arraycopy(mac.doFinal(BLOB_MAC), 0, rawMacKey, 0, rawMacKey.length);
|
||||
|
||||
mDataKey = new SecretKeySpec(rawDataKey, "AES");
|
||||
mMacKey = new SecretKeySpec(rawMacKey, "HmacSHA256");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
final RowBuilder row = result.newRow();
|
||||
row.add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID);
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY);
|
||||
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_label));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, DEFAULT_DOCUMENT_ID);
|
||||
row.add(Root.COLUMN_ICON, R.drawable.ic_lock_lock);
|
||||
|
||||
// Notify user in storage UI when key isn't hardware-backed
|
||||
if (!mHardwareBacked) {
|
||||
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.info_software));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private EncryptedDocument getDocument(long docId) throws GeneralSecurityException {
|
||||
final File file = new File(mDocumentsDir, String.valueOf(docId));
|
||||
return new EncryptedDocument(docId, file, mDataKey, mMacKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include metadata for a document in the given result cursor.
|
||||
*/
|
||||
private void includeDocument(MatrixCursor result, long docId)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if (!doc.getFile().exists()) {
|
||||
throw new FileNotFoundException("Missing document " + docId);
|
||||
}
|
||||
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
|
||||
int flags = 0;
|
||||
|
||||
final String mimeType = meta.optString(Document.COLUMN_MIME_TYPE);
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
flags |= Document.FLAG_SUPPORTS_DELETE;
|
||||
|
||||
final RowBuilder row = result.newRow();
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, meta.optLong(Document.COLUMN_DOCUMENT_ID));
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, meta.optString(Document.COLUMN_DISPLAY_NAME));
|
||||
row.add(Document.COLUMN_SIZE, meta.optLong(Document.COLUMN_SIZE));
|
||||
row.add(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, meta.optLong(Document.COLUMN_LAST_MODIFIED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
||||
throws FileNotFoundException {
|
||||
final long parentDocId = Long.parseLong(parentDocumentId);
|
||||
|
||||
// Allocate the next available ID
|
||||
final long childDocId;
|
||||
synchronized (mIdLock) {
|
||||
final SharedPreferences prefs = getContext()
|
||||
.getSharedPreferences(PREF_NEXT_ID, Context.MODE_PRIVATE);
|
||||
childDocId = prefs.getLong(PREF_NEXT_ID, 1);
|
||||
if (!prefs.edit().putLong(PREF_NEXT_ID, childDocId + 1).commit()) {
|
||||
throw new IllegalStateException("Failed to allocate document ID");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
initDocument(childDocId, mimeType, displayName);
|
||||
|
||||
// Update parent to reference new child
|
||||
final EncryptedDocument parentDoc = getDocument(parentDocId);
|
||||
final JSONObject parentMeta = parentDoc.readMetadata();
|
||||
parentMeta.accumulate(KEY_CHILDREN, childDocId);
|
||||
parentDoc.writeMetadataAndContent(parentMeta, null);
|
||||
|
||||
return String.valueOf(childDocId);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (JSONException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document on disk, writing an initial metadata section. Someone
|
||||
* might come back later to write contents.
|
||||
*/
|
||||
private void initDocument(long docId, String mimeType, String displayName)
|
||||
throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if (doc.getFile().exists()) return;
|
||||
|
||||
try {
|
||||
final JSONObject meta = new JSONObject();
|
||||
meta.put(Document.COLUMN_DOCUMENT_ID, docId);
|
||||
meta.put(Document.COLUMN_MIME_TYPE, mimeType);
|
||||
meta.put(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
|
||||
meta.put(KEY_CHILDREN, new JSONArray());
|
||||
}
|
||||
|
||||
doc.writeMetadataAndContent(meta, null);
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
final long docId = Long.parseLong(documentId);
|
||||
|
||||
try {
|
||||
// Delete given document, any children documents under it, and any
|
||||
// references to it from parents.
|
||||
deleteDocumentTree(docId);
|
||||
deleteDocumentReferences(docId);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively delete the given document and any children under it.
|
||||
*/
|
||||
private void deleteDocumentTree(long docId) throws IOException, GeneralSecurityException {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
try {
|
||||
if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
final long childDocId = children.getLong(i);
|
||||
deleteDocumentTree(childDocId);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (!doc.getFile().delete()) {
|
||||
throw new IOException("Failed to delete " + docId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any references to the given document, usually when included as a
|
||||
* child of another directory.
|
||||
*/
|
||||
private void deleteDocumentReferences(long docId) {
|
||||
for (String name : mDocumentsDir.list()) {
|
||||
try {
|
||||
final long parentDocId = Long.parseLong(name);
|
||||
final EncryptedDocument parentDoc = getDocument(parentDocId);
|
||||
final JSONObject meta = parentDoc.readMetadata();
|
||||
|
||||
if (Document.MIME_TYPE_DIR.equals(meta.getString(Document.COLUMN_MIME_TYPE))) {
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
if (maybeRemove(children, docId)) {
|
||||
Log.d(TAG, "Removed " + docId + " reference from " + name);
|
||||
parentDoc.writeMetadataAndContent(meta, null);
|
||||
|
||||
getContext().getContentResolver().notifyChange(
|
||||
DocumentsContract.buildChildDocumentsUri(AUTHORITY, name), null,
|
||||
false);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
} catch (JSONException e) {
|
||||
Log.w(TAG, "Failed to examine " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection)
|
||||
throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
try {
|
||||
includeDocument(result, Long.parseLong(documentId));
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(
|
||||
String parentDocumentId, String[] projection, String sortOrder)
|
||||
throws FileNotFoundException {
|
||||
final ExtrasMatrixCursor result = new ExtrasMatrixCursor(
|
||||
resolveDocumentProjection(projection));
|
||||
result.setNotificationUri(getContext().getContentResolver(),
|
||||
DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId));
|
||||
|
||||
// Notify user in storage UI when key isn't hardware-backed
|
||||
if (!mHardwareBacked) {
|
||||
result.putString(DocumentsContract.EXTRA_INFO,
|
||||
getContext().getString(R.string.info_software_detail));
|
||||
}
|
||||
|
||||
try {
|
||||
final EncryptedDocument doc = getDocument(Long.parseLong(parentDocumentId));
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
final JSONArray children = meta.getJSONArray(KEY_CHILDREN);
|
||||
for (int i = 0; i < children.length(); i++) {
|
||||
final long docId = children.getLong(i);
|
||||
includeDocument(result, docId);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (JSONException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(
|
||||
String documentId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException {
|
||||
final long docId = Long.parseLong(documentId);
|
||||
|
||||
try {
|
||||
final EncryptedDocument doc = getDocument(docId);
|
||||
if ("r".equals(mode)) {
|
||||
return startRead(doc);
|
||||
} else if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
return startWrite(doc);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported mode: " + mode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off a thread to handle a read request for the given document.
|
||||
* Internally creates a pipe and returns the read end for returning to a
|
||||
* remote process.
|
||||
*/
|
||||
private ParcelFileDescriptor startRead(final EncryptedDocument doc) throws IOException {
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
final ParcelFileDescriptor readEnd = pipe[0];
|
||||
final ParcelFileDescriptor writeEnd = pipe[1];
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
doc.readContent(writeEnd);
|
||||
Log.d(TAG, "Success reading " + doc);
|
||||
closeQuietly(writeEnd);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed reading " + doc, e);
|
||||
closeWithErrorQuietly(writeEnd, e.toString());
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed reading " + doc, e);
|
||||
closeWithErrorQuietly(writeEnd, e.toString());
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
return readEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kick off a thread to handle a write request for the given document.
|
||||
* Internally creates a pipe and returns the write end for returning to a
|
||||
* remote process.
|
||||
*/
|
||||
private ParcelFileDescriptor startWrite(final EncryptedDocument doc) throws IOException {
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
final ParcelFileDescriptor readEnd = pipe[0];
|
||||
final ParcelFileDescriptor writeEnd = pipe[1];
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final JSONObject meta = doc.readMetadata();
|
||||
doc.writeMetadataAndContent(meta, readEnd);
|
||||
Log.d(TAG, "Success writing " + doc);
|
||||
closeQuietly(readEnd);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed writing " + doc, e);
|
||||
closeWithErrorQuietly(readEnd, e.toString());
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w(TAG, "Failed writing " + doc, e);
|
||||
closeWithErrorQuietly(readEnd, e.toString());
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
return writeEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe remove the given value from a {@link JSONArray}.
|
||||
*
|
||||
* @return if the array was mutated.
|
||||
*/
|
||||
private static boolean maybeRemove(JSONArray array, long value) throws JSONException {
|
||||
boolean mutated = false;
|
||||
int i = 0;
|
||||
while (i < array.length()) {
|
||||
if (value == array.getLong(i)) {
|
||||
array.remove(i);
|
||||
mutated = true;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return mutated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple extension of {@link MatrixCursor} that makes it easy to provide a
|
||||
* {@link Bundle} of extras.
|
||||
*/
|
||||
private static class ExtrasMatrixCursor extends MatrixCursor {
|
||||
private Bundle mExtras;
|
||||
|
||||
public ExtrasMatrixCursor(String[] columnNames) {
|
||||
super(columnNames);
|
||||
}
|
||||
|
||||
public void putString(String key, String value) {
|
||||
if (mExtras == null) {
|
||||
mExtras = new Bundle();
|
||||
}
|
||||
mExtras.putString(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle getExtras() {
|
||||
return mExtras;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
samples/Vault/tests/Android.mk
Normal file
@@ -0,0 +1,13 @@
|
||||
LOCAL_PATH:= $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := android.test.runner
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_PACKAGE_NAME := VaultTests
|
||||
LOCAL_INSTRUMENTATION_FOR := Vault
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
14
samples/Vault/tests/AndroidManifest.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.vault.tests">
|
||||
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner" />
|
||||
</application>
|
||||
|
||||
<instrumentation
|
||||
android:name="android.test.InstrumentationTestRunner"
|
||||
android:targetPackage="com.example.android.vault"
|
||||
android:label="Vault tests" />
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.vault;
|
||||
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import android.test.suitebuilder.annotation.MediumTest;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.DigestException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Tests for {@link EncryptedDocument}.
|
||||
*/
|
||||
@MediumTest
|
||||
public class EncryptedDocumentTest extends AndroidTestCase {
|
||||
|
||||
private File mFile;
|
||||
|
||||
private SecretKey mDataKey = new SecretKeySpec(new byte[] {
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
|
||||
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }, "AES");
|
||||
|
||||
private SecretKey mMacKey = new SecretKeySpec(new byte[] {
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
|
||||
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02 }, "AES");
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mFile = new File(getContext().getFilesDir(), "meow");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
|
||||
for (File f : getContext().getFilesDir().listFiles()) {
|
||||
f.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public void testEmptyFile() throws Exception {
|
||||
mFile.createNewFile();
|
||||
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
|
||||
try {
|
||||
doc.readMetadata();
|
||||
fail("expected metadata to throw");
|
||||
} catch (IOException expected) {
|
||||
}
|
||||
|
||||
try {
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
doc.readContent(pipe[1]);
|
||||
fail("expected content to throw");
|
||||
} catch (IOException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testNormalMetadataAndContents() throws Exception {
|
||||
final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8);
|
||||
testMetadataAndContents(content);
|
||||
}
|
||||
|
||||
public void testGiantMetadataAndContents() throws Exception {
|
||||
// try with content size of prime number >1MB
|
||||
final byte[] content = new byte[1298047];
|
||||
Arrays.fill(content, (byte) 0x42);
|
||||
testMetadataAndContents(content);
|
||||
}
|
||||
|
||||
private void testMetadataAndContents(byte[] content) throws Exception {
|
||||
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
final byte[] beforeContent = content;
|
||||
|
||||
final ParcelFileDescriptor[] beforePipe = ParcelFileDescriptor.createReliablePipe();
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
final FileOutputStream os = new FileOutputStream(beforePipe[1].getFileDescriptor());
|
||||
try {
|
||||
os.write(beforeContent);
|
||||
beforePipe[1].close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
// fully write metadata and content
|
||||
final JSONObject before = new JSONObject();
|
||||
before.put("meow", "cake");
|
||||
doc.writeMetadataAndContent(before, beforePipe[0]);
|
||||
|
||||
// now go back and verify we can read
|
||||
final JSONObject after = doc.readMetadata();
|
||||
assertEquals("cake", after.getString("meow"));
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final ParcelFileDescriptor[] afterPipe = ParcelFileDescriptor.createReliablePipe();
|
||||
final byte[] afterContent = new byte[beforeContent.length];
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
final FileInputStream is = new FileInputStream(afterPipe[0].getFileDescriptor());
|
||||
try {
|
||||
int i = 0;
|
||||
while (i < afterContent.length) {
|
||||
int n = is.read(afterContent, i, afterContent.length - i);
|
||||
i += n;
|
||||
}
|
||||
afterPipe[0].close();
|
||||
latch.countDown();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
doc.readContent(afterPipe[1]);
|
||||
latch.await(5, TimeUnit.SECONDS);
|
||||
|
||||
MoreAsserts.assertEquals(beforeContent, afterContent);
|
||||
}
|
||||
|
||||
public void testNormalMetadataOnly() throws Exception {
|
||||
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
|
||||
// write only metadata
|
||||
final JSONObject before = new JSONObject();
|
||||
before.put("lol", "wut");
|
||||
doc.writeMetadataAndContent(before, null);
|
||||
|
||||
// verify we can read
|
||||
final JSONObject after = doc.readMetadata();
|
||||
assertEquals("wut", after.getString("lol"));
|
||||
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
try {
|
||||
doc.readContent(pipe[1]);
|
||||
fail("found document content");
|
||||
} catch (IOException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testCopiedFile() throws Exception {
|
||||
final EncryptedDocument doc1 = new EncryptedDocument(1, mFile, mDataKey, mMacKey);
|
||||
final EncryptedDocument doc4 = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
|
||||
// write values for doc1 into file
|
||||
final JSONObject meta1 = new JSONObject();
|
||||
meta1.put("key1", "value1");
|
||||
doc1.writeMetadataAndContent(meta1, null);
|
||||
|
||||
// now try reading as doc4, which should fail
|
||||
try {
|
||||
doc4.readMetadata();
|
||||
fail("somehow read without checking docid");
|
||||
} catch (DigestException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testBitTwiddle() throws Exception {
|
||||
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
|
||||
// write some metadata
|
||||
final JSONObject before = new JSONObject();
|
||||
before.put("twiddle", "twiddle");
|
||||
doc.writeMetadataAndContent(before, null);
|
||||
|
||||
final RandomAccessFile f = new RandomAccessFile(mFile, "rw");
|
||||
f.seek(f.length() - 4);
|
||||
f.write(0x00);
|
||||
f.close();
|
||||
|
||||
try {
|
||||
doc.readMetadata();
|
||||
fail("somehow passed hmac");
|
||||
} catch (DigestException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
public void testErrorAbortsWrite() throws Exception {
|
||||
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
|
||||
|
||||
// write initial metadata
|
||||
final JSONObject init = new JSONObject();
|
||||
init.put("color", "red");
|
||||
doc.writeMetadataAndContent(init, null);
|
||||
|
||||
// try writing with a pipe that reports failure
|
||||
final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8);
|
||||
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
final FileOutputStream os = new FileOutputStream(pipe[1].getFileDescriptor());
|
||||
try {
|
||||
os.write(content);
|
||||
pipe[1].closeWithError("ZOMG");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
|
||||
final JSONObject second = new JSONObject();
|
||||
second.put("color", "blue");
|
||||
try {
|
||||
doc.writeMetadataAndContent(second, pipe[0]);
|
||||
fail("somehow wrote without error");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
// verify that original metadata still in place
|
||||
final JSONObject after = doc.readMetadata();
|
||||
assertEquals("red", after.getString("color"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.vault;
|
||||
|
||||
import static com.example.android.vault.VaultProvider.AUTHORITY;
|
||||
import static com.example.android.vault.VaultProvider.DEFAULT_DOCUMENT_ID;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.database.Cursor;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.test.AndroidTestCase;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Tests for {@link VaultProvider}.
|
||||
*/
|
||||
public class VaultProviderTest extends AndroidTestCase {
|
||||
|
||||
private static final String MIME_TYPE_DEFAULT = "text/plain";
|
||||
|
||||
private ContentProviderClient mClient;
|
||||
private VaultProvider mProvider;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
mClient = getContext().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
||||
mProvider = (VaultProvider) mClient.getLocalContentProvider();
|
||||
mProvider.wipeAllContents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
|
||||
mClient.release();
|
||||
}
|
||||
|
||||
public void testDeleteDirectory() throws Exception {
|
||||
Cursor c;
|
||||
|
||||
final String file = mProvider.createDocument(
|
||||
DEFAULT_DOCUMENT_ID, MIME_TYPE_DEFAULT, "file");
|
||||
final String dir = mProvider.createDocument(
|
||||
DEFAULT_DOCUMENT_ID, Document.MIME_TYPE_DIR, "dir");
|
||||
|
||||
final String dirfile = mProvider.createDocument(
|
||||
dir, MIME_TYPE_DEFAULT, "dirfile");
|
||||
final String dirdir = mProvider.createDocument(
|
||||
dir, Document.MIME_TYPE_DIR, "dirdir");
|
||||
|
||||
final String dirdirfile = mProvider.createDocument(
|
||||
dirdir, MIME_TYPE_DEFAULT, "dirdirfile");
|
||||
|
||||
// verify everything is in place
|
||||
c = mProvider.queryChildDocuments(DEFAULT_DOCUMENT_ID, null, null);
|
||||
assertContains(c, "file", "dir");
|
||||
c = mProvider.queryChildDocuments(dir, null, null);
|
||||
assertContains(c, "dirfile", "dirdir");
|
||||
|
||||
// should remove children and parent ref
|
||||
mProvider.deleteDocument(dir);
|
||||
|
||||
c = mProvider.queryChildDocuments(DEFAULT_DOCUMENT_ID, null, null);
|
||||
assertContains(c, "file");
|
||||
|
||||
mProvider.queryDocument(file, null);
|
||||
|
||||
try { mProvider.queryDocument(dir, null); } catch (Exception expected) { }
|
||||
try { mProvider.queryDocument(dirfile, null); } catch (Exception expected) { }
|
||||
try { mProvider.queryDocument(dirdir, null); } catch (Exception expected) { }
|
||||
try { mProvider.queryDocument(dirdirfile, null); } catch (Exception expected) { }
|
||||
}
|
||||
|
||||
private static void assertContains(Cursor c, String... docs) {
|
||||
final HashSet<String> set = new HashSet<String>();
|
||||
while (c.moveToNext()) {
|
||||
set.add(c.getString(c.getColumnIndex(Document.COLUMN_DISPLAY_NAME)));
|
||||
}
|
||||
|
||||
for (String doc : docs) {
|
||||
assertTrue(doc, set.contains(doc));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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.activityinstrumentation"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="17" />
|
||||
|
||||
<application android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
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>
|
||||
13
samples/browseable/ActivityInstrumentation/_index.jd
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
|
||||
page.tags="ActivityInstrumentation"
|
||||
sample.group=Testing
|
||||
@jd:body
|
||||
|
||||
<p>This sample demonstrates how to use an
|
||||
{@link android.test.InstrumentationTestCase} to test the internal state of an
|
||||
{@link android.app.Activity}.</p>
|
||||
<p>To learn more about using Android's custom testing framework, see
|
||||
<a href="{@docRoot}training/activity-testing/index.html">Testing Your
|
||||
Android Activity</a>.</p>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
36
samples/browseable/ActivityInstrumentation/res/layout/activity_main.xml
Executable file
@@ -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>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<RelativeLayout 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:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/instructions"
|
||||
android:id="@+id/instructions"/>
|
||||
|
||||
<Spinner
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/spinner"
|
||||
android:layout_below="@+id/instructions"
|
||||
android:layout_centerHorizontal="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<!-- Customize dimensions originally defined in res/values/dimens.xml (such as
|
||||
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here. -->
|
||||
<dimen name="activity_horizontal_margin">128dp</dimen>
|
||||
</resources>
|
||||
@@ -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">ActivityInstrumentation</string>
|
||||
<string name="intro_message">
|
||||
<![CDATA[
|
||||
|
||||
|
||||
This sample provides a basic example of using an InstrumentationTest to probe the
|
||||
internal state of an Activity.
|
||||
|
||||
|
||||
]]>
|
||||
</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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="instructions">The value of the spinner below should be persisted when this activity is destroyed.</string>
|
||||
</resources>
|
||||
@@ -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>
|
||||
@@ -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.activityinstrumentation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Basic activity with a spinner. The spinner should persist its position to disk every time a
|
||||
* new selection is made.
|
||||
*/
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
/** Shared preferences key: Holds spinner position. Must not be negative. */
|
||||
private static final String PREF_SPINNER_POS = "spinner_pos";
|
||||
/** Magic constant to indicate that no value is stored for PREF_SPINNER_POS. */
|
||||
private static final int PREF_SPINNER_VALUE_ISNULL = -1;
|
||||
/** Values for display in spinner. */
|
||||
private static final String[] SPINNER_VALUES = new String[] {
|
||||
"Select Weather...", "Sunny", "Partly Cloudy", "Cloudy", "Rain", "Snow", "Hurricane"};
|
||||
|
||||
// Constants representing each of the options in SPINNER_VALUES. Declared package-private
|
||||
// so that they can be accessed from our test suite.
|
||||
static final int WEATHER_NOSELECTION = 0;
|
||||
static final int WEATHER_SUNNY = 1;
|
||||
static final int WEATHER_PARTLY_CLOUDY = 2;
|
||||
static final int WEATHER_CLOUDY = 3;
|
||||
static final int WEATHER_RAIN = 4;
|
||||
static final int WEATHER_SNOW = 5;
|
||||
static final int WEATHER_HURRICANE = 6;
|
||||
|
||||
/** Handle to default shared preferences for this activity. */
|
||||
private SharedPreferences mPrefs;
|
||||
/** Handle to the spinner in this Activity's layout. */
|
||||
private Spinner mSpinner;
|
||||
|
||||
/**
|
||||
* Setup activity state.
|
||||
*
|
||||
* @param savedInstanceState
|
||||
*/
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Inflate UI from res/layout/activity_main.xml
|
||||
setContentView(R.layout.sample_main);
|
||||
|
||||
// Get handle to default shared preferences for this activity
|
||||
mPrefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
|
||||
|
||||
// Populate spinner with sample values from an array
|
||||
mSpinner = (Spinner) findViewById(R.id.spinner);
|
||||
mSpinner.setAdapter(
|
||||
new ArrayAdapter<String>(
|
||||
this, // Context
|
||||
android.R.layout.simple_list_item_1, // Layout
|
||||
new ArrayList<String>(Arrays.asList(SPINNER_VALUES)) // Data source
|
||||
));
|
||||
|
||||
// Read in a sample value, if it's not set.
|
||||
int selection = mPrefs.getInt(PREF_SPINNER_POS, PREF_SPINNER_VALUE_ISNULL);
|
||||
if (selection != PREF_SPINNER_VALUE_ISNULL) {
|
||||
mSpinner.setSelection(selection);
|
||||
}
|
||||
|
||||
// Callback to persist spinner data whenever a new value is selected. This will be the
|
||||
// focus of our sample unit test.
|
||||
mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
|
||||
// The methods below commit the ID of the currently selected item in the spinner
|
||||
// to disk, using a SharedPreferences file.
|
||||
//
|
||||
// Note: A common mistake here is to forget to call .commit(). Try removing this
|
||||
// statement and running the tests to watch them fail.
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
mPrefs.edit().putInt(PREF_SPINNER_POS, position).commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> parent) {
|
||||
mPrefs.edit().remove(PREF_SPINNER_POS).commit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
56
samples/browseable/AdvancedImmersiveMode/AndroidManifest.xml
Normal file
@@ -0,0 +1,56 @@
|
||||
<?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.
|
||||
-->
|
||||
|
||||
<!-- the versionCode is an integer representation of this version of your application. New
|
||||
versions get higher numbers, so the upgrade system can avoid dealing with the ambiguity
|
||||
of "1.9" vs "1.10". versionName, on the other hand, can be whatever you want, as the code
|
||||
that handles upgrading Android apps between versions on your device just ignores it.-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.advancedimmersivemode"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
|
||||
<!-- This sample is to demonstrate features released in API 19.
|
||||
So while it would technically run on an earlier version of Android,
|
||||
there wouldn't be much point) -->
|
||||
<uses-sdk android:minSdkVersion="19" android:targetSdkVersion="19" />
|
||||
<!-- allowBackup declares if the app can be part of device-wide backups such as "adb backup" -->
|
||||
<!-- theme is a way of applying UI decisions across your entire application. You can also
|
||||
define it on a per-application basis. -->
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!-- Every activity needs its own Manifest element. The intent-filter contained in the
|
||||
element declares the intents that can be used to activate this Activity. For instance,
|
||||
the one below flags this Activity as a "main" entry point of this app, and suitable
|
||||
for creating a shortcut to in the Launcher. If you wanted your app to have 5
|
||||
different Activities available in the launcher, you could just make 5 activities
|
||||
with that intent filter. Please don't do that. Just because it's a good example
|
||||
doesn't mean it's a good idea. -->
|
||||
<activity android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:uiOptions="splitActionBarWhenNarrow">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
14
samples/browseable/AdvancedImmersiveMode/_index.jd
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
|
||||
|
||||
page.tags="AdvancedImmersiveMode"
|
||||
sample.group=UI
|
||||
@jd:body
|
||||
|
||||
<p>Android 4.4 introduces a way for you to provide a more immersive screen
|
||||
experience in your app, by letting users show or hide the status bar and
|
||||
navigation bar with a swipe.</p>
|
||||
<p>This sample demonstrates how this features interacts with some of the other
|
||||
UI flags related to full-screen apps. The sample also shows how to implement a
|
||||
"sticky" mode, which re-hides the bars a few seconds after the user swipes
|
||||
them back in.</p>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
38
samples/browseable/AdvancedImmersiveMode/res/layout/activity_main.xml
Executable file
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
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="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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/intro_message" />
|
||||
<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" />
|
||||
</LinearLayout>
|
||||
21
samples/browseable/AdvancedImmersiveMode/res/menu/main.xml
Normal file
@@ -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/sample_action"
|
||||
android:showAsAction="ifRoom|withText"
|
||||
android:title="@string/sample_action" />
|
||||
</menu>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">AdvancedImmersiveMode</string>
|
||||
<string name="intro_message">
|
||||
<![CDATA[
|
||||
|
||||
|
||||
\"Immersive Mode\" is a new UI mode which improves \"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
|
||||
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.
|
||||
|
||||
|
||||
]]>
|
||||
</string>
|
||||
</resources>
|
||||