Merge branch 'mnc-mr-docs' into mnc-ub-dev
Large merge to reconnect automerger for docs branch to mainline. Issue: 28000173 Change-Id: Ie403bcaf1c148c59be3a514613f7f1605146e66a
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
]]>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
11
samples/browseable/AsymmetricFingerprintDialog/_index.jd
Normal 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>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 196 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
Copyright 2013 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Semantic definitions -->
|
||||
|
||||
<dimen name="horizontal_page_margin">@dimen/margin_huge</dimen>
|
||||
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Copyright 2013 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Widget.SampleMessage">
|
||||
<item name="android:textAppearance">?android:textAppearanceLarge</item>
|
||||
<item name="android:lineSpacingMultiplier">1.2</item>
|
||||
<item name="android:shadowDy">-6.5</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
Copyright 2013 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Define standard dimensions to comply with Holo-style grids and rhythm. -->
|
||||
|
||||
<dimen name="margin_tiny">4dp</dimen>
|
||||
<dimen name="margin_small">8dp</dimen>
|
||||
<dimen name="margin_medium">16dp</dimen>
|
||||
<dimen name="margin_large">32dp</dimen>
|
||||
<dimen name="margin_huge">64dp</dimen>
|
||||
|
||||
<!-- Semantic definitions -->
|
||||
|
||||
<dimen name="horizontal_page_margin">@dimen/margin_medium</dimen>
|
||||
<dimen name="vertical_page_margin">@dimen/margin_medium</dimen>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||