Merge branch 'mnc-mr-docs' into mnc-ub-dev

Large merge to reconnect automerger for docs branch to mainline.

Issue: 28000173
Change-Id: Ie403bcaf1c148c59be3a514613f7f1605146e66a
This commit is contained in:
Trevor Johns
2016-04-05 20:26:07 -07:00
695 changed files with 17726 additions and 1028 deletions

View File

@@ -330,6 +330,10 @@ developers/build/prebuilts/gradle/RuntimePermissionsBasic sam
developers/build/prebuilts/gradle/ActiveNotifications samples/${PLATFORM_NAME}/notification/ActiveNotifications
developers/build/prebuilts/gradle/Camera2Raw samples/${PLATFORM_NAME}/media/Camera2Raw
developers/build/prebuilts/gradle/AutoBackupForApps samples/${PLATFORM_NAME}/content/AutoBackupForApps
developers/build/prebuilts/gradle/DirectShare samples/${PLATFORM_NAME}/content/DirectShare
developers/build/prebuilts/gradle/MidiScope samples/${PLATFORM_NAME}/media/MidiScope
developers/build/prebuilts/gradle/MidiSynth samples/${PLATFORM_NAME}/media/MidiSynth
developers/build/prebuilts/gradle/AsymmetricFingerprintDialog samples/${PLATFORM_NAME}/security/AsymmetricFingerprintDialog
developers/build/prebuilts/androidtv samples/${PLATFORM_NAME}/androidtv
@@ -346,12 +350,14 @@ developers/build/prebuilts/gradle/JumpingJack samples/${PLATFO
developers/build/prebuilts/gradle/Notifications samples/${PLATFORM_NAME}/wearable/Notifications
developers/build/prebuilts/gradle/Quiz samples/${PLATFORM_NAME}/wearable/Quiz
developers/build/prebuilts/gradle/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant
developers/build/prebuilts/gradle/RuntimePermissionsWear samples/${PLATFORM_NAME}/wearable/RuntimePermissionsWear
developers/build/prebuilts/gradle/SkeletonWearableApp samples/${PLATFORM_NAME}/wearable/SkeletonWearableApp
developers/build/prebuilts/gradle/SpeedTracker samples/${PLATFORM_NAME}/wearable/SpeedTracker
developers/build/prebuilts/gradle/SynchronizedNotifications samples/${PLATFORM_NAME}/wearable/SynchronizedNotifications
developers/build/prebuilts/gradle/Timer samples/${PLATFORM_NAME}/wearable/Timer
developers/build/prebuilts/gradle/WatchFace samples/${PLATFORM_NAME}/wearable/WatchFace
developers/build/prebuilts/gradle/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub
developers/build/prebuilts/gradle/WearSpeakerSample samples/${PLATFORM_NAME}/wearable/WearSpeakerSample
developers/build/prebuilts/gradle/XYZTouristAttractions samples/${PLATFORM_NAME}/wearable/XYZTouristAttractions
# Old sample tree

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -18,16 +18,22 @@
package="com.example.android.wearable.agendadata">
<uses-sdk android:minSdkVersion="18"
android:targetSdkVersion="21" />
android:targetSdkVersion="23" />
<!-- BEGIN_INCLUDE(manifest) -->
<!-- Note that all required permissions are declared here in the Android manifest.
On Android M and above, use of these permissions is only requested at run time. -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- END_INCLUDE(manifest) -->
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/Theme.Holo.Light"
>
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light">
<meta-data
android:name="com.google.android.gms.version"

View File

@@ -15,10 +15,17 @@
-->
<LinearLayout
android:id="@+id/main_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/vertical_page_margin"
android:paddingLeft="@dimen/horizontal_page_margin"
android:paddingRight="@dimen/horizontal_page_margin"
android:paddingTop="@dimen/vertical_page_margin"
tools:context=".MainActivity">
<Button
android:layout_height="wrap_content"
@@ -37,7 +44,7 @@
android:textAppearance="?android:textAppearanceLarge"
android:layout_marginTop="6dp"
android:padding="6dp"
android:text="@string/log"
android:text="@string/log_label"
android:textAllCaps="true"
android:textColor="@android:color/white"
android:background="@android:color/black"/>

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -22,10 +22,10 @@
Syncs calendar events to your wearable at the press of a button, using the Wearable
DataApi to transmit data such as event time, description, and background image. The DataItems can be
deleted individually via an action on the event notifications, or all at once via a button on the
companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
success or failure.
DataApi to transmit data such as event time, description, and background image. The
DataItems can be deleted individually via an action on the event notifications, or all
at once via a button on the companion. When deleted using the notification action, a
ConfirmationActivity is used to indicate success or failure.
]]>

View File

@@ -17,5 +17,9 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="get_events">Sync calendar events to wearable</string>
<string name="delete_events">Delete calendar events from wearable</string>
<string name="log">Log</string>
<string name="log_label">Deletion Log:</string>
<string name="permisions_granted">Permissions granted. Send Calendar events to Wear device.</string>
<string name="permissions_denied">Permission requests were denied. Can\'t send calendar events.</string>
<string name="permissions_rationale"><![CDATA[Calendar & Contact permissions are required to push calendar to Wear device.]]></string>
<string name="ok">OK</string>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -250,6 +250,11 @@ public class CalendarQueryService extends IntentService
public PutDataMapRequest toPutDataMapRequest(){
final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
makeDataItemPath(eventId, begin));
/* In most cases (as in this one), you don't need your DataItem appear instantly. By
default, delivery of normal DataItems to the Wear network might be delayed in order to
improve battery life for user devices. However, if you can't tolerate a delay in the
sync of your DataItems, you can mark them as urgent via setUrgent().
*/
DataMap data = putDataMapRequest.getDataMap();
data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
data.putLong(ID, id);

View File

@@ -18,11 +18,16 @@ package com.example.android.wearable.agendadata;
import static com.example.android.wearable.agendadata.Constants.TAG;
import android.app.Activity;
import android.Manifest;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ScrollView;
@@ -40,27 +45,42 @@ import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
import java.util.List;
/**
* Syncs or deletes calendar events (event time, description, and background image) to your
* Wearable via the Wearable DataApi at the click of a button. Includes code to handle dynamic M+
* permissions as well.
*/
public class MainActivity extends AppCompatActivity implements
NodeApi.NodeListener,
ConnectionCallbacks,
OnConnectionFailedListener,
ActivityCompat.OnRequestPermissionsResultCallback {
public class MainActivity extends Activity implements NodeApi.NodeListener, ConnectionCallbacks,
OnConnectionFailedListener {
/** Request code for launching the Intent to resolve Google Play services errors. */
/* Request code for launching the Intent to resolve Google Play services errors. */
private static final int REQUEST_RESOLVE_ERROR = 1000;
/* Id to identify calendar and contact permissions request. */
private static final int REQUEST_CALENDAR_AND_CONTACTS = 0;
private GoogleApiClient mGoogleApiClient;
private boolean mResolvingError = false;
private TextView mLogTextView;
ScrollView mScroller;
private View mLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mLayout = findViewById(R.id.main_layout);
mLogTextView = (TextView) findViewById(R.id.log);
mScroller = (ScrollView) findViewById(R.id.scroller);
mGoogleApiClient = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.addConnectionCallbacks(this)
@@ -85,11 +105,94 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
super.onStop();
}
public void onGetEventsClicked(View v) {
public void onGetEventsClicked(View view) {
Log.i(TAG, "onGetEventsClicked(): Checking permission.");
// BEGIN_INCLUDE(calendar_and_contact_permissions)
// Check if the Calendar permission is already available.
boolean calendarApproved =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR)
== PackageManager.PERMISSION_GRANTED;
boolean contactsApproved =
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
== PackageManager.PERMISSION_GRANTED;
if (!calendarApproved || !contactsApproved) {
// Calendar and/or Contact permissions have not been granted.
requestCalendarAndContactPermissions();
} else {
// Calendar permissions is already available, start service
Log.i(TAG, "Permissions already granted. Starting service.");
pushCalendarToWear();
}
// END_INCLUDE(calendar_and_contact_permissions)
}
private void pushCalendarToWear() {
startService(new Intent(this, CalendarQueryService.class));
}
public void onDeleteEventsClicked(View v) {
/*
* Requests Calendar and Contact permissions.
* If the permission has been denied previously, a SnackBar will prompt the user to grant the
* permission, otherwise it is requested directly.
*/
private void requestCalendarAndContactPermissions() {
Log.i(TAG, "CALENDAR permission has NOT been granted. Requesting permission.");
// BEGIN_INCLUDE(calendar_and_contact_permissions_request)
boolean showCalendarPermissionRationale =
ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CALENDAR);
boolean showContactsPermissionRationale =
ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS);
if (showCalendarPermissionRationale || showContactsPermissionRationale) {
/*
* Provide an additional rationale to the user if the permission was not granted and
* the user would benefit from additional context for the use of the permission. For
* example, if the user has previously denied the permission.
*/
Log.i(TAG, "Display calendar & contact permissions rationale for additional context.");
Snackbar.make(mLayout, R.string.permissions_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[] {
Manifest.permission.READ_CALENDAR,
Manifest.permission.READ_CONTACTS},
REQUEST_CALENDAR_AND_CONTACTS);
}
})
.show();
} else {
// Calendar/Contact permissions have not been granted yet. Request it directly.
ActivityCompat.requestPermissions(
this,
new String[]{
Manifest.permission.READ_CALENDAR,
Manifest.permission.READ_CONTACTS
},
REQUEST_CALENDAR_AND_CONTACTS);
}
// END_INCLUDE(calendar_and_contact_permissions_request)
}
public void onDeleteEventsClicked(View view) {
if (mGoogleApiClient.isConnected()) {
Wearable.DataApi.getDataItems(mGoogleApiClient)
.setResultCallback(new ResultCallback<DataItemBuffer>() {
@@ -100,9 +203,8 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
deleteDataItems(result);
} else {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG,"onDeleteEventsClicked(): failed to get Data "
Log.d(TAG, "onDeleteEventsClicked(): failed to get Data "
+ "Items");
}
}
} finally {
@@ -120,9 +222,11 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
if (mGoogleApiClient.isConnected()) {
for (final DataItem dataItem : dataItemList) {
final Uri dataItemUri = dataItem.getUri();
// In a real calendar application, this might delete the corresponding calendar
// event from the calendar data provider. In this sample, we simply delete the
// DataItem, but leave the phone's calendar data intact.
/*
* In a real calendar application, this might delete the corresponding calendar
* events from the calendar data provider. However, we simply delete the DataItem,
* but leave the phone's calendar data intact for this simple sample.
*/
Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri)
.setResultCallback(new ResultCallback<DataApi.DeleteDataItemsResult>() {
@Override
@@ -141,17 +245,6 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
}
}
private void appendLog(final String s) {
mLogTextView.post(new Runnable() {
@Override
public void run() {
mLogTextView.append(s);
mLogTextView.append("\n");
mScroller.fullScroll(View.FOCUS_DOWN);
}
});
}
@Override
public void onPeerConnected(Node peer) {
appendLog("Device connected");
@@ -165,7 +258,7 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
@Override
public void onConnected(Bundle connectionHint) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Connected to Google Api Service");
Log.d(TAG, "Connected to Google Api Service.");
}
mResolvingError = false;
Wearable.NodeApi.addListener(mGoogleApiClient, this);
@@ -193,10 +286,77 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
} catch (IntentSender.SendIntentException e) {
// There was an error with the resolution intent. Try again.
mResolvingError = false;
mGoogleApiClient.connect();
}
} else {
mResolvingError = false;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onActivityResult request/result codes: " + requestCode + "/" + resultCode);
}
if (requestCode == REQUEST_RESOLVE_ERROR) {
mResolvingError = false;
if (resultCode == RESULT_OK) {
// Make sure the app is not already connected or attempting to connect
if (!mGoogleApiClient.isConnecting() && !mGoogleApiClient.isConnected()) {
mGoogleApiClient.connect();
}
}
}
}
/**
* Callback received when a permissions request has been completed.
*/
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
}
if (requestCode == REQUEST_CALENDAR_AND_CONTACTS) {
// BEGIN_INCLUDE(permissions_result)
// Received permission result for calendar permission.
Log.i(TAG, "Received response for Calendar permission request.");
// Check if all required permissions have been granted.
if ((grantResults.length == 2)
&& (grantResults[0] == PackageManager.PERMISSION_GRANTED)
&& (grantResults[1] == PackageManager.PERMISSION_GRANTED)) {
// Calendar/Contact permissions have been granted, pull all calendar events
Log.i(TAG, "All permission has now been granted. Showing preview.");
Snackbar.make(mLayout, R.string.permisions_granted, Snackbar.LENGTH_SHORT).show();
pushCalendarToWear();
} else {
Log.i(TAG, "CALENDAR and/or CONTACT permissions were NOT granted.");
Snackbar.make(mLayout, R.string.permissions_denied, Snackbar.LENGTH_SHORT).show();
}
// END_INCLUDE(permissions_result)
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
private void appendLog(final String s) {
mLogTextView.post(new Runnable() {
@Override
public void run() {
mLogTextView.append(s);
mLogTextView.append("\n");
mScroller.fullScroll(View.FOCUS_DOWN);
}
});
}
}

View File

@@ -18,7 +18,7 @@
package="com.example.android.wearable.agendadata" >
<uses-sdk android:minSdkVersion="20"
android:targetSdkVersion="21" />
android:targetSdkVersion="22" />
<uses-feature android:name="android.hardware.type.watch" />

View File

@@ -72,7 +72,7 @@ public class HomeListenerService extends WearableListenerService {
if (event.getType() == DataEvent.TYPE_DELETED) {
deleteDataItem(event.getDataItem());
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
UpdateNotificationForDataItem(event.getDataItem());
updateNotificationForDataItem(event.getDataItem());
}
}
}
@@ -89,7 +89,7 @@ public class HomeListenerService extends WearableListenerService {
/**
* Posts a local notification to show calendar card.
*/
private void UpdateNotificationForDataItem(DataItem dataItem) {
private void updateNotificationForDataItem(DataItem dataItem) {
DataMapItem mapDataItem = DataMapItem.fromDataItem(dataItem);
DataMap data = mapDataItem.getDataMap();

View File

@@ -6,9 +6,9 @@ sample.group=Wearable
<p>
Syncs calendar events to your wearable at the press of a button, using the Wearable
DataApi to transmit data such as event time, description, and background image. The DataItems can be
deleted individually via an action on the event notifications, or all at once via a button on the
companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
success or failure.
DataApi to transmit data such as event time, description, and background image. The
DataItems can be deleted individually via an action on the event notifications, or all
at once via a button on the companion. When deleted using the notification action, a
ConfirmationActivity is used to indicate success or failure.
</p>

View File

@@ -119,6 +119,67 @@
</LinearLayout>
<LinearLayout
android:id="@+id/bundle_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile"/>
<EditText
android:id="@+id/profile_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.75"
android:hint="@string/name"/>
<EditText
android:id="@+id/profile_age"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.25"
android:hint="@string/age"
android:inputType="number"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/bundle_array_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/items_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:text="@string/items"/>
<LinearLayout
android:id="@+id/items"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@id/items_label"
android:orientation="vertical"
android:paddingEnd="16dp"
android:paddingStart="16dp"/>
<Button
android:id="@+id/item_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/items"
android:layout_toEndOf="@id/items_label"
android:text="@string/add"/>
</RelativeLayout>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:hint="@string/key"/>
<EditText
android:id="@+id/value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginStart="16dp"
android:hint="@string/value"/>
<Button
android:id="@+id/ok"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@android:string/ok"/>
</LinearLayout>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="key:value"/>
<Button
android:id="@+id/item_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/remove"/>
</LinearLayout>

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -29,4 +29,14 @@
<string name="number">Number: </string>
<string name="rank">Rank: </string>
<string name="approvals">Approvals: </string>
<string name="profile">Profile: </string>
<string name="name">Name</string>
<string name="age">Age</string>
<string name="items">Items: </string>
<string name="add">Add</string>
<string name="key">Key</string>
<string name="value">Value</string>
<string name="remove">Remove</string>
<string name="item">%1$s: %2$s</string>
<string name="add_item">Add a new item</string>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -22,7 +22,10 @@ import android.content.Context;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.Editable;
@@ -33,23 +36,28 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
* sample.
*/
public class AppRestrictionEnforcerFragment extends Fragment implements
CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
View.OnClickListener, ItemAddFragment.OnItemAddedListener {
/**
* Key for {@link SharedPreferences}
@@ -81,7 +89,24 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
*/
private static final String RESTRICTION_KEY_APPROVALS = "approvals";
/**
* Key for the bundle restriction in AppRestrictionSchema.
*/
private static final String RESTRICTION_KEY_PROFILE = "profile";
private static final String RESTRICTION_KEY_PROFILE_NAME = "name";
private static final String RESTRICTION_KEY_PROFILE_AGE = "age";
/**
* Key for the bundle array restriction in AppRestrictionSchema.
*/
private static final String RESTRICTION_KEY_ITEMS = "items";
private static final String RESTRICTION_KEY_ITEM_KEY = "key";
private static final String RESTRICTION_KEY_ITEM_VALUE = "value";
private static final String DELIMETER = ",";
private static final String SEPARATOR = ":";
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
/**
* Current status of the restrictions.
@@ -94,6 +119,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
private EditText mEditNumber;
private Spinner mSpinnerRank;
private LinearLayout mLayoutApprovals;
private EditText mEditProfileName;
private EditText mEditProfileAge;
private LinearLayout mLayoutItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -109,6 +137,19 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
mEditNumber = (EditText) view.findViewById(R.id.number);
mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
mLayoutApprovals = (LinearLayout) view.findViewById(R.id.approvals);
mEditProfileName = (EditText) view.findViewById(R.id.profile_name);
mEditProfileAge = (EditText) view.findViewById(R.id.profile_age);
mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
view.findViewById(R.id.item_add).setOnClickListener(this);
View bundleLayout = view.findViewById(R.id.bundle_layout);
View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout);
if (BUNDLE_SUPPORTED) {
bundleLayout.setVisibility(View.VISIBLE);
bundleArrayLayout.setVisibility(View.VISIBLE);
} else {
bundleLayout.setVisibility(View.GONE);
bundleArrayLayout.setVisibility(View.GONE);
}
}
@Override
@@ -156,6 +197,21 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
};
private TextWatcher mWatcherProfile = new EasyTextWatcher() {
@Override
public void afterTextChanged(Editable s) {
try {
String name = mEditProfileName.getText().toString();
String ageString = mEditProfileAge.getText().toString();
if (!TextUtils.isEmpty(ageString)) {
saveProfile(getActivity(), name, Integer.parseInt(ageString));
}
} catch (NumberFormatException e) {
Toast.makeText(getActivity(), "Not an integer!", Toast.LENGTH_SHORT).show();
}
}
};
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
switch (parent.getId()) {
@@ -171,9 +227,42 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
// Nothing to do
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.item_add:
new ItemAddFragment().show(getChildFragmentManager(), "dialog");
break;
case R.id.item_remove:
String key = (String) v.getTag();
removeItem(key);
mLayoutItems.removeView((View) v.getParent());
break;
}
}
@Override
public void onItemAdded(String key, String value) {
key = TextUtils.replace(key,
new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
value = TextUtils.replace(value,
new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
Map<String, String> items = new HashMap<>();
if (parcelables != null) {
for (Parcelable parcelable : parcelables) {
Bundle bundle = (Bundle) parcelable;
items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
}
}
items.put(key, value);
insertItemRow(LayoutInflater.from(getActivity()), key, value);
saveItems(getActivity(), items);
}
/**
* Loads the restrictions for the AppRestrictionSchema sample. In this implementation, we just
* read the default value for the "can_say_hello" restriction.
* Loads the restrictions for the AppRestrictionSchema sample.
*
* @param activity The activity
*/
@@ -203,6 +292,28 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER,
restriction.getAllSelectedStrings())),
DELIMETER));
} else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_PROFILE.equals(key)) {
String name = null;
int age = 0;
for (RestrictionEntry entry : restriction.getRestrictions()) {
String profileKey = entry.getKey();
if (RESTRICTION_KEY_PROFILE_NAME.equals(profileKey)) {
name = entry.getSelectedString();
} else if (RESTRICTION_KEY_PROFILE_AGE.equals(profileKey)) {
age = entry.getIntValue();
}
}
name = prefs.getString(RESTRICTION_KEY_PROFILE_NAME, name);
age = prefs.getInt(RESTRICTION_KEY_PROFILE_AGE, age);
updateProfile(name, age);
} else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) {
String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
HashMap<String, String> items = new HashMap<>();
for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
String[] strings = itemString.split(SEPARATOR, 2);
items.put(strings[0], strings[1]);
}
updateItems(activity, items);
}
}
}
@@ -251,6 +362,72 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
}
}
private void updateProfile(String name, int age) {
if (!BUNDLE_SUPPORTED) {
return;
}
Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
mEditProfileName.removeTextChangedListener(mWatcherProfile);
mEditProfileName.setText(name);
mEditProfileName.addTextChangedListener(mWatcherProfile);
mEditProfileAge.removeTextChangedListener(mWatcherProfile);
mEditProfileAge.setText(String.valueOf(age));
mEditProfileAge.addTextChangedListener((mWatcherProfile));
}
private void updateItems(Context context, Map<String, String> items) {
if (!BUNDLE_SUPPORTED) {
return;
}
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
LayoutInflater inflater = LayoutInflater.from(context);
mLayoutItems.removeAllViews();
for (String key : items.keySet()) {
insertItemRow(inflater, key, items.get(key));
}
}
private void insertItemRow(LayoutInflater inflater, String key, String value) {
View view = inflater.inflate(R.layout.item, mLayoutItems, false);
TextView textView = (TextView) view.findViewById(R.id.item_text);
textView.setText(getString(R.string.item, key, value));
Button remove = (Button) view.findViewById(R.id.item_remove);
remove.setTag(key);
remove.setOnClickListener(this);
mLayoutItems.addView(view);
}
@NonNull
private Bundle[] convertToBundles(Map<String, String> items) {
Bundle[] bundles = new Bundle[items.size()];
int i = 0;
for (String key : items.keySet()) {
Bundle bundle = new Bundle();
bundle.putString(RESTRICTION_KEY_ITEM_KEY, key);
bundle.putString(RESTRICTION_KEY_ITEM_VALUE, items.get(key));
bundles[i++] = bundle;
}
return bundles;
}
private void removeItem(String key) {
Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
if (parcelables != null) {
Map<String, String> items = new HashMap<>();
for (Parcelable parcelable : parcelables) {
Bundle bundle = (Bundle) parcelable;
if (!key.equals(bundle.getString(RESTRICTION_KEY_ITEM_KEY))) {
items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
}
}
saveItems(getActivity(), items);
}
}
/**
* Saves the value for the "cay_say_hello" restriction of AppRestrictionSchema.
*
@@ -333,6 +510,57 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
TextUtils.join(DELIMETER, approvals)).apply();
}
/**
* Saves the value for the "profile" restriction of AppRestrictionSchema.
*
* @param activity The activity
* @param name The value to be set for the "name" field.
* @param age The value to be set for the "age" field.
*/
private void saveProfile(Activity activity, String name, int age) {
if (!BUNDLE_SUPPORTED) {
return;
}
Bundle profile = new Bundle();
profile.putString(RESTRICTION_KEY_PROFILE_NAME, name);
profile.putInt(RESTRICTION_KEY_PROFILE_AGE, age);
mCurrentRestrictions.putBundle(RESTRICTION_KEY_PROFILE, profile);
saveRestrictions(activity);
editPreferences(activity).putString(RESTRICTION_KEY_PROFILE_NAME, name).apply();
}
/**
* Saves the value for the "items" restriction of AppRestrictionSchema.
*
* @param activity The activity.
* @param items The values.
*/
private void saveItems(Activity activity, Map<String, String> items) {
if (!BUNDLE_SUPPORTED) {
return;
}
mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
saveRestrictions(activity);
StringBuilder builder = new StringBuilder();
boolean first = true;
for (String key : items.keySet()) {
if (first) {
first = false;
} else {
builder.append(DELIMETER);
}
builder.append(key);
builder.append(SEPARATOR);
builder.append(items.get(key));
}
editPreferences(activity).putString(RESTRICTION_KEY_ITEMS, builder.toString()).apply();
}
/**
* Saves all the restrictions.
*
* @param activity The activity.
*/
private void saveRestrictions(Activity activity) {
DevicePolicyManager devicePolicyManager
= (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.apprestrictionenforcer;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.Toast;
/**
* Provides a dialog to create a new restriction item for the sample bundle array.
*/
public class ItemAddFragment extends DialogFragment implements View.OnClickListener {
public interface OnItemAddedListener {
void onItemAdded(String key, String value);
}
private OnItemAddedListener mListener;
private EditText mEditKey;
private EditText mEditValue;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Fragment parentFragment = getParentFragment();
mListener = (OnItemAddedListener) (parentFragment == null ? activity : parentFragment);
}
@Override
public void onDetach() {
mListener = null;
super.onDetach();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
getDialog().setTitle(R.string.add_item);
return inflater.inflate(R.layout.fragment_item_add, container, false);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
mEditKey = (EditText) view.findViewById(R.id.key);
mEditValue = (EditText) view.findViewById(R.id.value);
view.findViewById(R.id.ok).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.ok:
if (addItem()) {
dismiss();
}
break;
}
}
private boolean addItem() {
String key = mEditKey.getText().toString();
if (TextUtils.isEmpty(key)) {
Toast.makeText(getActivity(), "Input the key.", Toast.LENGTH_SHORT).show();
return false;
}
String value = mEditValue.getText().toString();
if (TextUtils.isEmpty(value)) {
Toast.makeText(getActivity(), "Input the value.", Toast.LENGTH_SHORT).show();
return false;
}
if (mListener != null) {
mListener.onItemAdded(key, value);
}
return true;
}
}

View File

@@ -59,7 +59,7 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_rank"/>
<include layout="@layout/separator"/>
<include layout="@layout/separator" android:id="@+id/bundle_separator"/>
<TextView
android:id="@+id/approvals_you_have"
@@ -68,6 +68,24 @@ limitations under the License.
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/approvals_you_have"/>
<include layout="@layout/separator"/>
<TextView
android:id="@+id/your_profile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_profile"/>
<include layout="@layout/separator" android:id="@+id/bundle_array_separator" />
<TextView
android:id="@+id/your_items"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
tools:text="@string/your_items"/>
</LinearLayout>
</ScrollView>

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -74,4 +74,23 @@ limitations under the License.
<string name="description_secret_code">This restriction is hidden and will not be shown to the administrator.</string>
<string name="default_secret_code">(Hidden restriction must have some default value)</string>
<!-- Bundle restriction -->
<string name="description_profile">Sample profile</string>
<string name="title_profile">Profile</string>
<string name="default_profile_name">John</string>
<string name="description_profile_name">The name of this person</string>
<string name="title_profile_name">Name</string>
<integer name="default_profile_age">25</integer>
<string name="description_profile_age">The age of this person</string>
<string name="title_profile_age">Age</string>
<!-- Bundle array restriction -->
<string name="description_items">Sample items</string>
<string name="title_items">Items</string>
<string name="title_item">Item</string>
<string name="title_key">Key</string>
<string name="title_value">Value</string>
</resources>

View File

@@ -25,5 +25,7 @@ limitations under the License.
<string name="your_rank">Your rank: %s</string>
<string name="approvals_you_have">Approvals you have: %s</string>
<string name="none">none</string>
<string name="your_profile">Your profile: %1$s (%2$d)</string>
<string name="your_items">Your items: %s</string>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,6 +20,7 @@ limitations under the License.
https://developer.android.com/reference/android/content/RestrictionsManager.html
-->
<!-- Boolean restriction -->
<restriction
android:defaultValue="@bool/default_can_say_hello"
android:description="@string/description_can_say_hello"
@@ -28,6 +28,7 @@ limitations under the License.
android:restrictionType="bool"
android:title="@string/title_can_say_hello"/>
<!-- String restriction -->
<restriction
android:defaultValue="@string/default_message"
android:description="@string/description_message"
@@ -35,6 +36,7 @@ limitations under the License.
android:restrictionType="string"
android:title="@string/title_message"/>
<!-- Integer restriction -->
<restriction
android:defaultValue="@integer/default_number"
android:description="@string/description_number"
@@ -42,6 +44,7 @@ limitations under the License.
android:restrictionType="integer"
android:title="@string/title_number"/>
<!-- Choice restriction -->
<restriction
android:defaultValue="@string/default_rank"
android:description="@string/description_rank"
@@ -51,6 +54,7 @@ limitations under the License.
android:restrictionType="choice"
android:title="@string/title_rank"/>
<!-- Multi-select restriction -->
<restriction
android:defaultValue="@array/default_approvals"
android:description="@string/description_approvals"
@@ -60,6 +64,7 @@ limitations under the License.
android:restrictionType="multi-select"
android:title="@string/title_approvals"/>
<!-- Hidden restriction -->
<restriction
android:defaultValue="@string/default_secret_code"
android:description="@string/description_secret_code"
@@ -67,4 +72,46 @@ limitations under the License.
android:restrictionType="hidden"
android:title="@string/title_secret_code"/>
<!-- Bundle restriction; useful for grouping restrictions -->
<restriction
android:description="@string/description_profile"
android:key="profile"
android:restrictionType="bundle"
android:title="@string/title_profile">
<restriction
android:defaultValue="@string/default_profile_name"
android:description="@string/description_profile_name"
android:key="name"
android:restrictionType="string"
android:title="@string/title_profile_name"/>
<restriction
android:defaultValue="@integer/default_profile_age"
android:description="@string/description_profile_age"
android:key="age"
android:restrictionType="integer"
android:title="@string/title_profile_age"/>
</restriction>
<!-- Bundle array restriction -->
<restriction
android:description="@string/description_items"
android:key="items"
android:restrictionType="bundle_array"
android:title="@string/title_items">
<!-- Bundle array must have one bundle restriction -->
<restriction
android:key="item"
android:restrictionType="bundle"
android:title="@string/title_item">
<restriction
android:key="key"
android:restrictionType="string"
android:title="@string/title_key"/>
<restriction
android:key="value"
android:restrictionType="string"
android:title="@string/title_value"/>
</restriction>
</restriction>
</restrictions>

View File

@@ -19,7 +19,9 @@ package com.example.android.apprestrictionschema;
import android.content.Context;
import android.content.RestrictionEntry;
import android.content.RestrictionsManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.text.TextUtils;
@@ -49,6 +51,14 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
private static final String KEY_NUMBER = "number";
private static final String KEY_RANK = "rank";
private static final String KEY_APPROVALS = "approvals";
private static final String KEY_PROFILE = "profile";
private static final String KEY_PROFILE_NAME = "name";
private static final String KEY_PROFILE_AGE = "age";
private static final String KEY_ITEMS = "items";
private static final String KEY_ITEM_KEY = "key";
private static final String KEY_ITEM_VALUE = "value";
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
// Message to show when the button is clicked (String restriction)
private String mMessage;
@@ -59,6 +69,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
private TextView mTextNumber;
private TextView mTextRank;
private TextView mTextApprovals;
private TextView mTextProfile;
private TextView mTextItems;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -73,7 +85,22 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextNumber = (TextView) view.findViewById(R.id.your_number);
mTextRank = (TextView) view.findViewById(R.id.your_rank);
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
View bundleSeparator = view.findViewById(R.id.bundle_separator);
mTextProfile = (TextView) view.findViewById(R.id.your_profile);
View bundleArraySeparator = view.findViewById(R.id.bundle_array_separator);
mTextItems = (TextView) view.findViewById(R.id.your_items);
mButtonSayHello.setOnClickListener(this);
if (BUNDLE_SUPPORTED) {
bundleSeparator.setVisibility(View.VISIBLE);
mTextProfile.setVisibility(View.VISIBLE);
bundleArraySeparator.setVisibility(View.VISIBLE);
mTextItems.setVisibility(View.VISIBLE);
} else {
bundleSeparator.setVisibility(View.GONE);
mTextProfile.setVisibility(View.GONE);
bundleArraySeparator.setVisibility(View.GONE);
mTextItems.setVisibility(View.GONE);
}
}
@Override
@@ -86,7 +113,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
RestrictionsManager manager =
(RestrictionsManager) getActivity().getSystemService(Context.RESTRICTIONS_SERVICE);
Bundle restrictions = manager.getApplicationRestrictions();
List<RestrictionEntry> entries = manager.getManifestRestrictions(getActivity().getApplicationContext().getPackageName());
List<RestrictionEntry> entries = manager.getManifestRestrictions(
getActivity().getApplicationContext().getPackageName());
for (RestrictionEntry entry : entries) {
String key = entry.getKey();
Log.d(TAG, "key: " + key);
@@ -100,6 +128,10 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
updateRank(entry, restrictions);
} else if (key.equals(KEY_APPROVALS)) {
updateApprovals(entry, restrictions);
} else if (key.equals(KEY_PROFILE)) {
updateProfile(entry, restrictions);
} else if (key.equals(KEY_ITEMS)) {
updateItems(entry, restrictions);
}
}
}
@@ -161,6 +193,67 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
mTextApprovals.setText(getString(R.string.approvals_you_have, text));
}
private void updateProfile(RestrictionEntry entry, Bundle restrictions) {
if (!BUNDLE_SUPPORTED) {
return;
}
String name = null;
int age = 0;
if (restrictions == null || !restrictions.containsKey(KEY_PROFILE)) {
RestrictionEntry[] entries = entry.getRestrictions();
for (RestrictionEntry profileEntry : entries) {
String key = profileEntry.getKey();
if (key.equals(KEY_PROFILE_NAME)) {
name = profileEntry.getSelectedString();
} else if (key.equals(KEY_PROFILE_AGE)) {
age = profileEntry.getIntValue();
}
}
} else {
Bundle profile = restrictions.getBundle(KEY_PROFILE);
if (profile != null) {
name = profile.getString(KEY_PROFILE_NAME);
age = profile.getInt(KEY_PROFILE_AGE);
}
}
mTextProfile.setText(getString(R.string.your_profile, name, age));
}
private void updateItems(RestrictionEntry entry, Bundle restrictions) {
if (!BUNDLE_SUPPORTED) {
return;
}
StringBuilder builder = new StringBuilder();
if (restrictions != null) {
Parcelable[] parcelables = restrictions.getParcelableArray(KEY_ITEMS);
if (parcelables != null && parcelables.length > 0) {
Bundle[] items = new Bundle[parcelables.length];
for (int i = 0; i < parcelables.length; i++) {
items[i] = (Bundle) parcelables[i];
}
boolean first = true;
for (Bundle item : items) {
if (!item.containsKey(KEY_ITEM_KEY) || !item.containsKey(KEY_ITEM_VALUE)) {
continue;
}
if (first) {
first = false;
} else {
builder.append(", ");
}
builder.append(item.getString(KEY_ITEM_KEY));
builder.append(":");
builder.append(item.getString(KEY_ITEM_VALUE));
}
} else {
builder.append(getString(R.string.none));
}
} else {
builder.append(getString(R.string.none));
}
mTextItems.setText(getString(R.string.your_items, builder));
}
@Override
public void onClick(View view) {
switch (view.getId()) {

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -16,11 +16,13 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="@dimen/margin_medium">
android:padding="@dimen/margin_medium"
>
<Button android:id="@+id/button_open_usage_setting"
android:layout_width="wrap_content"
@@ -50,6 +52,8 @@
android:scrollbars="vertical"
android:drawSelectorOnTop="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="match_parent"
app:layoutManager="LinearLayoutManager"
/>
</LinearLayout>

View File

@@ -17,6 +17,6 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
</resources>

View File

@@ -18,7 +18,7 @@
<resources>
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -18,7 +18,7 @@
<!-- Activity themes -->
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
<style name="Theme.Base" parent="android:Theme.Light" />
<style name="Theme.Sample" parent="Theme.Base" />

View File

@@ -24,7 +24,6 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.Fragment;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
@@ -91,10 +90,9 @@ public class AppUsageStatisticsFragment extends Fragment {
public void onViewCreated(View rootView, Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
mLayoutManager = new LinearLayoutManager(getActivity());
mUsageListAdapter = new UsageListAdapter();
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_app_usage);
mRecyclerView.setLayoutManager(mLayoutManager);
mLayoutManager = mRecyclerView.getLayoutManager();
mRecyclerView.scrollToPosition(0);
mRecyclerView.setAdapter(mUsageListAdapter);
mOpenUsageSettingButton = (Button) rootView.findViewById(R.id.button_open_usage_setting);
@@ -197,7 +195,7 @@ public class AppUsageStatisticsFragment extends Fragment {
@Override
public int compare(UsageStats left, UsageStats right) {
return (int) (right.getLastTimeUsed() - left.getLastTimeUsed());
return Long.compare(right.getLastTimeUsed(), left.getLastTimeUsed());
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2015 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT 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.asymmetricfingerprintdialog"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<application
android:name=".InjectedApplication"
android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/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>
<activity
android:name=".SettingsActivity"
android:label="@string/action_settings" />
</application>
</manifest>

View File

@@ -0,0 +1,11 @@
page.tags="AsymmetricFingerprintDialog"
sample.group=Security
@jd:body
<p>
This sample demonstrates how you can use registered fingerprints to authenticate the user
before proceeding some actions such as purchasing an item. This version uses asymmetric keys.
</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#fefefe"/>
<corners
android:radius="2dp" />
</shape>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40.0dp"
android:height="40.0dp"
android:viewportWidth="40.0"
android:viewportHeight="40.0">
<path
android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z"
android:fillColor="#F4511E"/>
<path
android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40.0dp"
android:height="40.0dp"
android:viewportWidth="40.0"
android:viewportHeight="40.0">
<path
android:pathData="M20.0,20.0m-20.0,0.0a20.0,20.0 0.0,1.0 1.0,40.0 0.0a20.0,20.0 0.0,1.0 1.0,-40.0 0.0"
android:fillColor="#009688"/>
<path
android:pathData="M11.2,21.41l1.63,-1.619999 4.17,4.169998 10.59,-10.589999 1.619999,1.63 -12.209999,12.209999z"
android:fillColor="#FFFFFF"/>
</vector>

View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="150dp"
android:layout_height="150dp"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:layout_gravity="center_horizontal"
android:scaleType="fitCenter"
android:src="@drawable/android_robot"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
android:background="@drawable/card"
android:elevation="4dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Material.Headline"
android:text="@string/item_title"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Material.Body2"
android:textColor="?android:attr/colorAccent"
android:text="@string/item_price"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="@android:style/TextAppearance.Material.Body1"
android:textColor="?android:attr/textColorSecondary"
android:text="@string/item_description"/>
</LinearLayout>
<Button style="@android:style/Widget.Material.Button.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="4dp"
android:layout_gravity="end"
android:textColor="?android:attr/textColorPrimaryInverse"
android:text="@string/purchase"
android:id="@+id/purchase_button"
android:layout_alignParentEnd="true"/>
<TextView
android:id="@+id/confirmation_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAppearance="@android:style/TextAppearance.Material.Body2"
android:textColor="?android:attr/colorAccent"
android:text="@string/purchase_done"
android:visibility="gone"/>
<TextView
android:id="@+id/encrypted_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAppearance="@android:style/TextAppearance.Material.Body2"
android:textColor="?android:attr/colorAccent"
android:text="@string/purchase_done"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/backup_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="16dp"
android:paddingBottom="8dp">
<FrameLayout
android:id="@+id/description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Material.Subhead"
android:text="@string/password_description"
android:id="@+id/password_description"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.Material.Subhead"
android:text="@string/new_fingerprint_enrolled_description"
android:id="@+id/new_fingerprint_enrolled_description"
android:visibility="gone"
android:textColor="?android:attr/textColorSecondary" />
</FrameLayout>
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:ems="10"
android:hint="@string/password"
android:imeOptions="actionGo"
android:id="@+id/password"
android:layout_below="@+id/description"
android:layout_marginTop="16dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_alignParentStart="true" />
<CheckBox
android:id="@+id/use_fingerprint_in_future_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/password"
android:layout_alignParentStart="true"
android:layout_marginTop="16dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:checked="true"
android:visibility="gone"
android:text="@string/use_fingerprint_in_future" />
</RelativeLayout>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT 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">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/fingerprint_dialog_content" />
<include
layout="@layout/fingerprint_dialog_backup"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
android:id="@+id/buttonPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:gravity="bottom"
style="?android:attr/buttonBarStyle">
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<Button
android:id="@+id/cancel_button"
style="?android:attr/buttonBarNegativeButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/second_dialog_button"
style="?android:attr/buttonBarPositiveButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fingerprint_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:paddingTop="16dp">
<TextView
android:id="@+id/fingerprint_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:text="@string/fingerprint_description"
android:textAppearance="@android:style/TextAppearance.Material.Subhead"
android:textColor="?android:attr/textColorSecondary"/>
<ImageView
android:id="@+id/fingerprint_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/fingerprint_description"
android:layout_marginTop="20dp"
android:src="@drawable/ic_fp_40px" />
<TextView
android:id="@+id/fingerprint_status"
style="@android:style/TextAppearance.Material.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/fingerprint_icon"
android:layout_alignTop="@+id/fingerprint_icon"
android:layout_marginStart="16dp"
android:layout_toEndOf="@+id/fingerprint_icon"
android:gravity="center_vertical"
android:text="@string/fingerprint_hint"
android:textColor="@color/hint_color" />
</RelativeLayout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity">
<item android:id="@+id/action_settings" android:title="@string/action_settings"
android:orderInCategory="100" android:showAsAction="never" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

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,22 @@
<!--
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" />
</resources>

View File

@@ -0,0 +1,21 @@
<?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>
</resources>

View File

@@ -0,0 +1,24 @@
<?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>
<!-- Activity themes -->
<style name="Theme.Base" parent="android:Theme.Material.Light">
</style>
</resources>

View File

@@ -0,0 +1,30 @@
<?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">AsymmetricFingerprintDialog</string>
<string name="intro_message">
<![CDATA[
This sample demonstrates how you can use registered fingerprints to authenticate the user
before proceeding some actions such as purchasing an item. This version uses asymmetric keys.
]]>
</string>
</resources>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<resources>
<color name="warning_color">#f4511e</color>
<color name="hint_color">#42000000</color>
<color name="success_color">#009688</color>
</resources>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT 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="action_settings">Settings</string>
<string name="cancel">Cancel</string>
<string name="use_password">Use password</string>
<string name="sign_in">Sign in</string>
<string name="ok">Ok</string>
<string name="password">Password</string>
<string name="fingerprint_description">Confirm fingerprint to continue</string>
<string name="fingerprint_hint">Touch sensor</string>
<string name="password_description">Enter your store password to continue</string>
<string name="purchase">Purchase</string>
<string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string>
<string name="fingerprint_success">Fingerprint recognized</string>
<string name="item_title">White Mesh Pluto Backpack</string>
<string name="item_price">$62.68</string>
<string name="item_description">Mesh backpack in white. Black textile trim throughout.</string>
<string name="purchase_done">Purchase successful</string>
<string name="purchase_fail">Purchase failed</string>
<string name="new_fingerprint_enrolled_description">A new fingerprint was added to this device, so your password is required.</string>
<string name="use_fingerprint_in_future">Use fingerprint in the future</string>
<string name="use_fingerprint_to_authenticate_title">Use fingerprint to authenticate</string>
<string name="use_fingerprint_to_authenticate_key" >use_fingerprint_to_authenticate_key</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,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.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,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2015 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<CheckBoxPreference
android:key="@string/use_fingerprint_to_authenticate_key"
android:title="@string/use_fingerprint_to_authenticate_title"
android:persistent="true"
android:defaultValue="true" />
</PreferenceScreen>

View File

@@ -0,0 +1,320 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.asymmetricfingerprintdialog;
import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
import com.example.android.asymmetricfingerprintdialog.server.Transaction;
import android.app.Activity;
import android.app.DialogFragment;
import android.content.SharedPreferences;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import javax.inject.Inject;
/**
* A dialog which uses fingerprint APIs to authenticate the user, and falls back to password
* authentication if fingerprint is not available.
*/
public class FingerprintAuthenticationDialogFragment extends DialogFragment
implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback {
private Button mCancelButton;
private Button mSecondDialogButton;
private View mFingerprintContent;
private View mBackupContent;
private EditText mPassword;
private CheckBox mUseFingerprintFutureCheckBox;
private TextView mPasswordDescriptionTextView;
private TextView mNewFingerprintEnrolledTextView;
private Stage mStage = Stage.FINGERPRINT;
private FingerprintManager.CryptoObject mCryptoObject;
private FingerprintUiHelper mFingerprintUiHelper;
private MainActivity mActivity;
@Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder;
@Inject InputMethodManager mInputMethodManager;
@Inject SharedPreferences mSharedPreferences;
@Inject StoreBackend mStoreBackend;
@Inject
public FingerprintAuthenticationDialogFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Do not create a new Fragment when the Activity is re-created such as orientation changes.
setRetainInstance(true);
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog);
// We register a new user account here. Real apps should do this with proper UIs.
enroll();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
getDialog().setTitle(getString(R.string.sign_in));
View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
mCancelButton = (Button) v.findViewById(R.id.cancel_button);
mCancelButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dismiss();
}
});
mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button);
mSecondDialogButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mStage == Stage.FINGERPRINT) {
goToBackup();
} else {
verifyPassword();
}
}
});
mFingerprintContent = v.findViewById(R.id.fingerprint_container);
mBackupContent = v.findViewById(R.id.backup_container);
mPassword = (EditText) v.findViewById(R.id.password);
mPassword.setOnEditorActionListener(this);
mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description);
mUseFingerprintFutureCheckBox = (CheckBox)
v.findViewById(R.id.use_fingerprint_in_future_check);
mNewFingerprintEnrolledTextView = (TextView)
v.findViewById(R.id.new_fingerprint_enrolled_description);
mFingerprintUiHelper = mFingerprintUiHelperBuilder.build(
(ImageView) v.findViewById(R.id.fingerprint_icon),
(TextView) v.findViewById(R.id.fingerprint_status), this);
updateStage();
// If fingerprint authentication is not available, switch immediately to the backup
// (password) screen.
if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) {
goToBackup();
}
return v;
}
@Override
public void onResume() {
super.onResume();
if (mStage == Stage.FINGERPRINT) {
mFingerprintUiHelper.startListening(mCryptoObject);
}
}
public void setStage(Stage stage) {
mStage = stage;
}
@Override
public void onPause() {
super.onPause();
mFingerprintUiHelper.stopListening();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = (MainActivity) activity;
}
/**
* Sets the crypto object to be passed in when authenticating with fingerprint.
*/
public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
mCryptoObject = cryptoObject;
}
/**
* Switches to backup (password) screen. This either can happen when fingerprint is not
* available or the user chooses to use the password authentication method by pressing the
* button. This can also happen when the user had too many fingerprint attempts.
*/
private void goToBackup() {
mStage = Stage.PASSWORD;
updateStage();
mPassword.requestFocus();
// Show the keyboard.
mPassword.postDelayed(mShowKeyboardRunnable, 500);
// Fingerprint is not used anymore. Stop listening for it.
mFingerprintUiHelper.stopListening();
}
/**
* Enrolls a user to the fake backend.
*/
private void enroll() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey();
// Provide the public key to the backend. In most cases, the key needs to be transmitted
// to the backend over the network, for which Key.getEncoded provides a suitable wire
// format (X.509 DER-encoded). The backend can then create a PublicKey instance from the
// X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently
// needed on API Level 23 (Android M) due to a platform bug which prevents the use of
// Android Keystore public keys when their private keys require user authentication.
// This conversion creates a new public key which is not backed by Android Keystore and
// thus is not affected by the bug.
KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm());
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded());
PublicKey verificationKey = factory.generatePublic(spec);
mStoreBackend.enroll("user", "password", verificationKey);
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException |
IOException | InvalidKeySpecException e) {
e.printStackTrace();
}
}
/**
* Checks whether the current entered password is correct, and dismisses the the dialog and lets
* the activity know about the result.
*/
private void verifyPassword() {
Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) {
return;
}
if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
mUseFingerprintFutureCheckBox.isChecked());
editor.apply();
if (mUseFingerprintFutureCheckBox.isChecked()) {
// Re-create the key so that fingerprints including new ones are validated.
mActivity.createKeyPair();
mStage = Stage.FINGERPRINT;
}
}
mPassword.setText("");
mActivity.onPurchased(null);
dismiss();
}
private final Runnable mShowKeyboardRunnable = new Runnable() {
@Override
public void run() {
mInputMethodManager.showSoftInput(mPassword, 0);
}
};
private void updateStage() {
switch (mStage) {
case FINGERPRINT:
mCancelButton.setText(R.string.cancel);
mSecondDialogButton.setText(R.string.use_password);
mFingerprintContent.setVisibility(View.VISIBLE);
mBackupContent.setVisibility(View.GONE);
break;
case NEW_FINGERPRINT_ENROLLED:
// Intentional fall through
case PASSWORD:
mCancelButton.setText(R.string.cancel);
mSecondDialogButton.setText(R.string.ok);
mFingerprintContent.setVisibility(View.GONE);
mBackupContent.setVisibility(View.VISIBLE);
if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) {
mPasswordDescriptionTextView.setVisibility(View.GONE);
mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE);
mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE);
}
break;
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_GO) {
verifyPassword();
return true;
}
return false;
}
@Override
public void onAuthenticated() {
// Callback from FingerprintUiHelper. Let the activity know that authentication was
// successful.
mPassword.setText("");
Signature signature = mCryptoObject.getSignature();
// Include a client nonce in the transaction so that the nonce is also signed by the private
// key and the backend can verify that the same nonce can't be used to prevent replay
// attacks.
Transaction transaction = new Transaction("user", 1, new SecureRandom().nextLong());
try {
signature.update(transaction.toByteArray());
byte[] sigBytes = signature.sign();
if (mStoreBackend.verify(transaction, sigBytes)) {
mActivity.onPurchased(sigBytes);
dismiss();
} else {
mActivity.onPurchaseFailed();
dismiss();
}
} catch (SignatureException e) {
throw new RuntimeException(e);
}
}
@Override
public void onError() {
goToBackup();
}
/**
* Enumeration to indicate which authentication method the user is trying to authenticate with.
*/
public enum Stage {
FINGERPRINT,
NEW_FINGERPRINT_ENROLLED,
PASSWORD
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.asymmetricfingerprintdialog;
import com.example.android.asymmetricfingerprintdialog.server.StoreBackend;
import com.example.android.asymmetricfingerprintdialog.server.StoreBackendImpl;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.hardware.fingerprint.FingerprintManager;
import android.preference.PreferenceManager;
import android.security.keystore.KeyProperties;
import android.view.inputmethod.InputMethodManager;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Signature;
import dagger.Module;
import dagger.Provides;
/**
* Dagger module for Fingerprint APIs.
*/
@Module(
library = true,
injects = {MainActivity.class}
)
public class FingerprintModule {
private final Context mContext;
public FingerprintModule(Context context) {
mContext = context;
}
@Provides
public Context providesContext() {
return mContext;
}
@Provides
public FingerprintManager providesFingerprintManager(Context context) {
return context.getSystemService(FingerprintManager.class);
}
@Provides
public KeyguardManager providesKeyguardManager(Context context) {
return context.getSystemService(KeyguardManager.class);
}
@Provides
public KeyStore providesKeystore() {
try {
return KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
throw new RuntimeException("Failed to get an instance of KeyStore", e);
}
}
@Provides
public KeyPairGenerator providesKeyPairGenerator() {
try {
return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
}
}
@Provides
public Signature providesSignature(KeyStore keyStore) {
try {
return Signature.getInstance("SHA256withECDSA");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Failed to get an instance of Signature", e);
}
}
@Provides
public InputMethodManager providesInputMethodManager(Context context) {
return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
}
@Provides
public SharedPreferences providesSharedPreferences(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
@Provides
public StoreBackend providesStoreBackend() {
return new StoreBackendImpl();
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.asymmetricfingerprintdialog;
import com.google.common.annotations.VisibleForTesting;
import android.hardware.fingerprint.FingerprintManager;
import android.os.CancellationSignal;
import android.widget.ImageView;
import android.widget.TextView;
import javax.inject.Inject;
/**
* Small helper class to manage text/icon around fingerprint authentication UI.
*/
public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback {
@VisibleForTesting static final long ERROR_TIMEOUT_MILLIS = 1600;
@VisibleForTesting static final long SUCCESS_DELAY_MILLIS = 1300;
private final FingerprintManager mFingerprintManager;
private final ImageView mIcon;
private final TextView mErrorTextView;
private final Callback mCallback;
private CancellationSignal mCancellationSignal;
@VisibleForTesting boolean mSelfCancelled;
/**
* Builder class for {@link FingerprintUiHelper} in which injected fields from Dagger
* holds its fields and takes other arguments in the {@link #build} method.
*/
public static class FingerprintUiHelperBuilder {
private final FingerprintManager mFingerPrintManager;
@Inject
public FingerprintUiHelperBuilder(FingerprintManager fingerprintManager) {
mFingerPrintManager = fingerprintManager;
}
public FingerprintUiHelper build(ImageView icon, TextView errorTextView, Callback callback) {
return new FingerprintUiHelper(mFingerPrintManager, icon, errorTextView,
callback);
}
}
/**
* Constructor for {@link FingerprintUiHelper}. This method is expected to be called from
* only the {@link FingerprintUiHelperBuilder} class.
*/
private FingerprintUiHelper(FingerprintManager fingerprintManager,
ImageView icon, TextView errorTextView, Callback callback) {
mFingerprintManager = fingerprintManager;
mIcon = icon;
mErrorTextView = errorTextView;
mCallback = callback;
}
public boolean isFingerprintAuthAvailable() {
return mFingerprintManager.isHardwareDetected()
&& mFingerprintManager.hasEnrolledFingerprints();
}
public void startListening(FingerprintManager.CryptoObject cryptoObject) {
if (!isFingerprintAuthAvailable()) {
return;
}
mCancellationSignal = new CancellationSignal();
mSelfCancelled = false;
mFingerprintManager
.authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null);
mIcon.setImageResource(R.drawable.ic_fp_40px);
}
public void stopListening() {
if (mCancellationSignal != null) {
mSelfCancelled = true;
mCancellationSignal.cancel();
mCancellationSignal = null;
}
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
if (!mSelfCancelled) {
showError(errString);
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onError();
}
}, ERROR_TIMEOUT_MILLIS);
}
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
showError(helpString);
}
@Override
public void onAuthenticationFailed() {
showError(mIcon.getResources().getString(
R.string.fingerprint_not_recognized));
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mIcon.setImageResource(R.drawable.ic_fingerprint_success);
mErrorTextView.setTextColor(
mErrorTextView.getResources().getColor(R.color.success_color, null));
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_success));
mIcon.postDelayed(new Runnable() {
@Override
public void run() {
mCallback.onAuthenticated();
}
}, SUCCESS_DELAY_MILLIS);
}
private void showError(CharSequence error) {
mIcon.setImageResource(R.drawable.ic_fingerprint_error);
mErrorTextView.setText(error);
mErrorTextView.setTextColor(
mErrorTextView.getResources().getColor(R.color.warning_color, null));
mErrorTextView.removeCallbacks(mResetErrorTextRunnable);
mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS);
}
@VisibleForTesting
Runnable mResetErrorTextRunnable = new Runnable() {
@Override
public void run() {
mErrorTextView.setTextColor(
mErrorTextView.getResources().getColor(R.color.hint_color, null));
mErrorTextView.setText(
mErrorTextView.getResources().getString(R.string.fingerprint_hint));
mIcon.setImageResource(R.drawable.ic_fp_40px);
}
};
public interface Callback {
void onAuthenticated();
void onError();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.asymmetricfingerprintdialog;
import android.app.Application;
import android.util.Log;
import dagger.ObjectGraph;
/**
* The Application class of the sample which holds the ObjectGraph in Dagger and enables
* dependency injection.
*/
public class InjectedApplication extends Application {
private static final String TAG = InjectedApplication.class.getSimpleName();
private ObjectGraph mObjectGraph;
@Override
public void onCreate() {
super.onCreate();
initObjectGraph(new FingerprintModule(this));
}
/**
* Initialize the Dagger module. Passing null or mock modules can be used for testing.
*
* @param module for Dagger
*/
public void initObjectGraph(Object module) {
mObjectGraph = module != null ? ObjectGraph.create(module) : null;
}
public void inject(Object object) {
if (mObjectGraph == null) {
// This usually happens during tests.
Log.i(TAG, "Object graph is not initialized.");
return;
}
mObjectGraph.inject(object);
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT 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.asymmetricfingerprintdialog;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Intent;
import android.content.SharedPreferences;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Bundle;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.ECGenParameterSpec;
import javax.inject.Inject;
/**
* Main entry point for the sample, showing a backpack and "Purchase" button.
*/
public class MainActivity extends Activity {
private static final String DIALOG_FRAGMENT_TAG = "myFragment";
/** Alias for our key in the Android Key Store */
public static final String KEY_NAME = "my_key";
@Inject KeyguardManager mKeyguardManager;
@Inject FingerprintManager mFingerprintManager;
@Inject FingerprintAuthenticationDialogFragment mFragment;
@Inject KeyStore mKeyStore;
@Inject KeyPairGenerator mKeyPairGenerator;
@Inject Signature mSignature;
@Inject SharedPreferences mSharedPreferences;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((InjectedApplication) getApplication()).inject(this);
setContentView(R.layout.activity_main);
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
if (!mKeyguardManager.isKeyguardSecure()) {
// Show a message that the user hasn't set up a fingerprint or lock screen.
Toast.makeText(this,
"Secure lock screen hasn't set up.\n"
+ "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint",
Toast.LENGTH_LONG).show();
purchaseButton.setEnabled(false);
return;
}
//noinspection ResourceType
if (!mFingerprintManager.hasEnrolledFingerprints()) {
purchaseButton.setEnabled(false);
// This happens when no fingerprints are registered.
Toast.makeText(this,
"Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint",
Toast.LENGTH_LONG).show();
return;
}
createKeyPair();
purchaseButton.setEnabled(true);
purchaseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
findViewById(R.id.confirmation_message).setVisibility(View.GONE);
findViewById(R.id.encrypted_message).setVisibility(View.GONE);
// Set up the crypto object for later. The object will be authenticated by use
// of the fingerprint.
if (initSignature()) {
// Show the fingerprint dialog. The user has the option to use the fingerprint with
// crypto, or you can fall back to using a server-side verified password.
mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature));
boolean useFingerprintPreference = mSharedPreferences
.getBoolean(getString(R.string.use_fingerprint_to_authenticate_key),
true);
if (useFingerprintPreference) {
mFragment.setStage(
FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT);
} else {
mFragment.setStage(
FingerprintAuthenticationDialogFragment.Stage.PASSWORD);
}
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
} else {
// This happens if the lock screen has been disabled or or a fingerprint got
// enrolled. Thus show the dialog to authenticate with their password first
// and ask the user if they want to authenticate with fingerprints in the
// future
mFragment.setStage(
FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED);
mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
}
}
});
}
/**
* Initialize the {@link Signature} instance with the created key in the
* {@link #createKeyPair()} method.
*
* @return {@code true} if initialization is successful, {@code false} if the lock screen has
* been disabled or reset after the key was generated, or if a fingerprint got enrolled after
* the key was generated.
*/
private boolean initSignature() {
try {
mKeyStore.load(null);
PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
mSignature.initSign(key);
return true;
} catch (KeyPermanentlyInvalidatedException e) {
return false;
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
| NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to init Cipher", e);
}
}
public void onPurchased(byte[] signature) {
showConfirmation(signature);
}
public void onPurchaseFailed() {
Toast.makeText(this, R.string.purchase_fail, Toast.LENGTH_SHORT).show();
}
// Show confirmation, if fingerprint was used show crypto information.
private void showConfirmation(byte[] encrypted) {
findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE);
if (encrypted != null) {
TextView v = (TextView) findViewById(R.id.encrypted_message);
v.setVisibility(View.VISIBLE);
v.setText(Base64.encodeToString(encrypted, 0 /* flags */));
}
}
/**
* Generates an asymmetric key pair in the Android Keystore. Every use of the private key must
* be authorized by the user authenticating with fingerprint. Public key use is unrestricted.
*/
public void createKeyPair() {
// The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
// for your flow. Use of keys is necessary if you need to know if the set of
// enrolled fingerprints has changed.
try {
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
mKeyPairGenerator.initialize(
new KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_SIGN)
.setDigests(KeyProperties.DIGEST_SHA256)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
// Require the user to authenticate with a fingerprint to authorize
// every use of the private key
.setUserAuthenticationRequired(true)
.build());
mKeyPairGenerator.generateKeyPair();
} catch (InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.action_settings) {
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
return super.onOptionsItemSelected(item);
}
}

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