Merge commit '54ec4ff7dfa9e8028f9da1986a73bddb1c00be93' into HEAD

This commit is contained in:
The Android Open Source Project
2013-12-05 17:25:54 -08:00
1024 changed files with 45637 additions and 2298 deletions

View File

@@ -1,3 +1,4 @@
page.keywords="Sensor", "Games", "Accelerometer"
page.tags="Sensor", "Games", "Accelerometer"
sample.group=Sensors
@jd:body

View File

@@ -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

View File

@@ -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) -->

View 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>

View 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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View 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>

View File

@@ -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>

View File

@@ -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) -->

View File

@@ -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>

View File

@@ -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 -->
<!-- ============================ -->

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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/

View 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>

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View 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>

View 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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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/

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View 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>

View 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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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>

View File

@@ -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 -->

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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");
}
}
}

View File

@@ -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);
}
}

View File

@@ -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!");
}
}
}
}

View File

@@ -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
View 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)

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="isAtLeastKitKat">true</bool>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="isAtLeastKitKat">false</bool>
</resources>

View 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>

View File

@@ -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));
}
}
}

View File

@@ -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);
}
}

View 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();
}
}
}

View 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;
}
}
}

View 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)

View 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>

View File

@@ -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"));
}
}

View File

@@ -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));
}
}
}

View File

@@ -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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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.
-->
<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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More