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/ActiveNotifications samples/${PLATFORM_NAME}/notification/ActiveNotifications
|
||||||
developers/build/prebuilts/gradle/Camera2Raw samples/${PLATFORM_NAME}/media/Camera2Raw
|
developers/build/prebuilts/gradle/Camera2Raw samples/${PLATFORM_NAME}/media/Camera2Raw
|
||||||
developers/build/prebuilts/gradle/AutoBackupForApps samples/${PLATFORM_NAME}/content/AutoBackupForApps
|
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
|
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/Notifications samples/${PLATFORM_NAME}/wearable/Notifications
|
||||||
developers/build/prebuilts/gradle/Quiz samples/${PLATFORM_NAME}/wearable/Quiz
|
developers/build/prebuilts/gradle/Quiz samples/${PLATFORM_NAME}/wearable/Quiz
|
||||||
developers/build/prebuilts/gradle/RecipeAssistant samples/${PLATFORM_NAME}/wearable/RecipeAssistant
|
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/SkeletonWearableApp samples/${PLATFORM_NAME}/wearable/SkeletonWearableApp
|
||||||
developers/build/prebuilts/gradle/SpeedTracker samples/${PLATFORM_NAME}/wearable/SpeedTracker
|
developers/build/prebuilts/gradle/SpeedTracker samples/${PLATFORM_NAME}/wearable/SpeedTracker
|
||||||
developers/build/prebuilts/gradle/SynchronizedNotifications samples/${PLATFORM_NAME}/wearable/SynchronizedNotifications
|
developers/build/prebuilts/gradle/SynchronizedNotifications samples/${PLATFORM_NAME}/wearable/SynchronizedNotifications
|
||||||
developers/build/prebuilts/gradle/Timer samples/${PLATFORM_NAME}/wearable/Timer
|
developers/build/prebuilts/gradle/Timer samples/${PLATFORM_NAME}/wearable/Timer
|
||||||
developers/build/prebuilts/gradle/WatchFace samples/${PLATFORM_NAME}/wearable/WatchFace
|
developers/build/prebuilts/gradle/WatchFace samples/${PLATFORM_NAME}/wearable/WatchFace
|
||||||
developers/build/prebuilts/gradle/WatchViewStub samples/${PLATFORM_NAME}/wearable/WatchViewStub
|
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
|
developers/build/prebuilts/gradle/XYZTouristAttractions samples/${PLATFORM_NAME}/wearable/XYZTouristAttractions
|
||||||
|
|
||||||
# Old sample tree
|
# Old sample tree
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -18,16 +18,22 @@
|
|||||||
package="com.example.android.wearable.agendadata">
|
package="com.example.android.wearable.agendadata">
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="18"
|
<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_CALENDAR" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
|
||||||
|
<!-- END_INCLUDE(manifest) -->
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:icon="@drawable/ic_launcher"
|
android:allowBackup="true"
|
||||||
android:label="@string/app_name"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:theme="@android:style/Theme.Holo.Light"
|
android:label="@string/app_name"
|
||||||
>
|
android:theme="@style/Theme.AppCompat.Light">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.version"
|
android:name="com.google.android.gms.version"
|
||||||
|
|||||||
@@ -15,10 +15,17 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/main_layout"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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_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
|
<Button
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -37,7 +44,7 @@
|
|||||||
android:textAppearance="?android:textAppearanceLarge"
|
android:textAppearance="?android:textAppearanceLarge"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:padding="6dp"
|
android:padding="6dp"
|
||||||
android:text="@string/log"
|
android:text="@string/log_label"
|
||||||
android:textAllCaps="true"
|
android:textAllCaps="true"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:background="@android:color/black"/>
|
android:background="@android:color/black"/>
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
|
|
||||||
|
|
||||||
Syncs calendar events to your wearable at the press of a button, using the Wearable
|
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
|
DataApi to transmit data such as event time, description, and background image. The
|
||||||
deleted individually via an action on the event notifications, or all at once via a button on the
|
DataItems can be deleted individually via an action on the event notifications, or all
|
||||||
companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
|
at once via a button on the companion. When deleted using the notification action, a
|
||||||
success or failure.
|
ConfirmationActivity is used to indicate success or failure.
|
||||||
|
|
||||||
|
|
||||||
]]>
|
]]>
|
||||||
|
|||||||
@@ -17,5 +17,9 @@
|
|||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="get_events">Sync calendar events to wearable</string>
|
<string name="get_events">Sync calendar events to wearable</string>
|
||||||
<string name="delete_events">Delete calendar events from 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>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,11 @@ public class CalendarQueryService extends IntentService
|
|||||||
public PutDataMapRequest toPutDataMapRequest(){
|
public PutDataMapRequest toPutDataMapRequest(){
|
||||||
final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
|
final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(
|
||||||
makeDataItemPath(eventId, begin));
|
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();
|
DataMap data = putDataMapRequest.getDataMap();
|
||||||
data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
|
data.putString(DATA_ITEM_URI, putDataMapRequest.getUri().toString());
|
||||||
data.putLong(ID, id);
|
data.putLong(ID, id);
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ package com.example.android.wearable.agendadata;
|
|||||||
|
|
||||||
import static com.example.android.wearable.agendadata.Constants.TAG;
|
import static com.example.android.wearable.agendadata.Constants.TAG;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.Manifest;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentSender;
|
import android.content.IntentSender;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
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.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ScrollView;
|
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.NodeApi;
|
||||||
import com.google.android.gms.wearable.Wearable;
|
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,
|
/* Request code for launching the Intent to resolve Google Play services errors. */
|
||||||
OnConnectionFailedListener {
|
|
||||||
|
|
||||||
/** Request code for launching the Intent to resolve Google Play services errors. */
|
|
||||||
private static final int REQUEST_RESOLVE_ERROR = 1000;
|
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 GoogleApiClient mGoogleApiClient;
|
||||||
private boolean mResolvingError = false;
|
private boolean mResolvingError = false;
|
||||||
|
|
||||||
private TextView mLogTextView;
|
private TextView mLogTextView;
|
||||||
ScrollView mScroller;
|
ScrollView mScroller;
|
||||||
|
|
||||||
|
private View mLayout;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
setContentView(R.layout.main);
|
setContentView(R.layout.main);
|
||||||
|
mLayout = findViewById(R.id.main_layout);
|
||||||
|
|
||||||
mLogTextView = (TextView) findViewById(R.id.log);
|
mLogTextView = (TextView) findViewById(R.id.log);
|
||||||
mScroller = (ScrollView) findViewById(R.id.scroller);
|
mScroller = (ScrollView) findViewById(R.id.scroller);
|
||||||
|
|
||||||
mGoogleApiClient = new GoogleApiClient.Builder(this)
|
mGoogleApiClient = new GoogleApiClient.Builder(this)
|
||||||
.addApi(Wearable.API)
|
.addApi(Wearable.API)
|
||||||
.addConnectionCallbacks(this)
|
.addConnectionCallbacks(this)
|
||||||
@@ -85,11 +105,94 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
|
|||||||
super.onStop();
|
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));
|
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()) {
|
if (mGoogleApiClient.isConnected()) {
|
||||||
Wearable.DataApi.getDataItems(mGoogleApiClient)
|
Wearable.DataApi.getDataItems(mGoogleApiClient)
|
||||||
.setResultCallback(new ResultCallback<DataItemBuffer>() {
|
.setResultCallback(new ResultCallback<DataItemBuffer>() {
|
||||||
@@ -100,9 +203,8 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
|
|||||||
deleteDataItems(result);
|
deleteDataItems(result);
|
||||||
} else {
|
} else {
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
Log.d(TAG,"onDeleteEventsClicked(): failed to get Data "
|
Log.d(TAG, "onDeleteEventsClicked(): failed to get Data "
|
||||||
+ "Items");
|
+ "Items");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -120,9 +222,11 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
|
|||||||
if (mGoogleApiClient.isConnected()) {
|
if (mGoogleApiClient.isConnected()) {
|
||||||
for (final DataItem dataItem : dataItemList) {
|
for (final DataItem dataItem : dataItemList) {
|
||||||
final Uri dataItemUri = dataItem.getUri();
|
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
|
* In a real calendar application, this might delete the corresponding calendar
|
||||||
// DataItem, but leave the phone's calendar data intact.
|
* 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)
|
Wearable.DataApi.deleteDataItems(mGoogleApiClient, dataItemUri)
|
||||||
.setResultCallback(new ResultCallback<DataApi.DeleteDataItemsResult>() {
|
.setResultCallback(new ResultCallback<DataApi.DeleteDataItemsResult>() {
|
||||||
@Override
|
@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
|
@Override
|
||||||
public void onPeerConnected(Node peer) {
|
public void onPeerConnected(Node peer) {
|
||||||
appendLog("Device connected");
|
appendLog("Device connected");
|
||||||
@@ -165,7 +258,7 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
|
|||||||
@Override
|
@Override
|
||||||
public void onConnected(Bundle connectionHint) {
|
public void onConnected(Bundle connectionHint) {
|
||||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||||
Log.d(TAG, "Connected to Google Api Service");
|
Log.d(TAG, "Connected to Google Api Service.");
|
||||||
}
|
}
|
||||||
mResolvingError = false;
|
mResolvingError = false;
|
||||||
Wearable.NodeApi.addListener(mGoogleApiClient, this);
|
Wearable.NodeApi.addListener(mGoogleApiClient, this);
|
||||||
@@ -193,10 +286,77 @@ public class MainActivity extends Activity implements NodeApi.NodeListener, Conn
|
|||||||
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
|
result.startResolutionForResult(this, REQUEST_RESOLVE_ERROR);
|
||||||
} catch (IntentSender.SendIntentException e) {
|
} catch (IntentSender.SendIntentException e) {
|
||||||
// There was an error with the resolution intent. Try again.
|
// There was an error with the resolution intent. Try again.
|
||||||
|
mResolvingError = false;
|
||||||
mGoogleApiClient.connect();
|
mGoogleApiClient.connect();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mResolvingError = false;
|
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" >
|
package="com.example.android.wearable.agendadata" >
|
||||||
|
|
||||||
<uses-sdk android:minSdkVersion="20"
|
<uses-sdk android:minSdkVersion="20"
|
||||||
android:targetSdkVersion="21" />
|
android:targetSdkVersion="22" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ public class HomeListenerService extends WearableListenerService {
|
|||||||
if (event.getType() == DataEvent.TYPE_DELETED) {
|
if (event.getType() == DataEvent.TYPE_DELETED) {
|
||||||
deleteDataItem(event.getDataItem());
|
deleteDataItem(event.getDataItem());
|
||||||
} else if (event.getType() == DataEvent.TYPE_CHANGED) {
|
} 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.
|
* Posts a local notification to show calendar card.
|
||||||
*/
|
*/
|
||||||
private void UpdateNotificationForDataItem(DataItem dataItem) {
|
private void updateNotificationForDataItem(DataItem dataItem) {
|
||||||
DataMapItem mapDataItem = DataMapItem.fromDataItem(dataItem);
|
DataMapItem mapDataItem = DataMapItem.fromDataItem(dataItem);
|
||||||
DataMap data = mapDataItem.getDataMap();
|
DataMap data = mapDataItem.getDataMap();
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ sample.group=Wearable
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
Syncs calendar events to your wearable at the press of a button, using the Wearable
|
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
|
DataApi to transmit data such as event time, description, and background image. The
|
||||||
deleted individually via an action on the event notifications, or all at once via a button on the
|
DataItems can be deleted individually via an action on the event notifications, or all
|
||||||
companion. When deleted using the notification action, a ConfirmationActivity is used to indicate
|
at once via a button on the companion. When deleted using the notification action, a
|
||||||
success or failure.
|
ConfirmationActivity is used to indicate success or failure.
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -119,6 +119,67 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</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>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -29,4 +29,14 @@
|
|||||||
<string name="number">Number: </string>
|
<string name="number">Number: </string>
|
||||||
<string name="rank">Rank: </string>
|
<string name="rank">Rank: </string>
|
||||||
<string name="approvals">Approvals: </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>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import android.content.Context;
|
|||||||
import android.content.RestrictionEntry;
|
import android.content.RestrictionEntry;
|
||||||
import android.content.RestrictionsManager;
|
import android.content.RestrictionsManager;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
@@ -33,23 +36,28 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.CompoundButton;
|
import android.widget.CompoundButton;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
|
* This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
|
||||||
* sample.
|
* sample.
|
||||||
*/
|
*/
|
||||||
public class AppRestrictionEnforcerFragment extends Fragment implements
|
public class AppRestrictionEnforcerFragment extends Fragment implements
|
||||||
CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
|
||||||
|
View.OnClickListener, ItemAddFragment.OnItemAddedListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key for {@link SharedPreferences}
|
* Key for {@link SharedPreferences}
|
||||||
@@ -81,7 +89,24 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
*/
|
*/
|
||||||
private static final String RESTRICTION_KEY_APPROVALS = "approvals";
|
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 DELIMETER = ",";
|
||||||
|
private static final String SEPARATOR = ":";
|
||||||
|
|
||||||
|
private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current status of the restrictions.
|
* Current status of the restrictions.
|
||||||
@@ -94,6 +119,9 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
private EditText mEditNumber;
|
private EditText mEditNumber;
|
||||||
private Spinner mSpinnerRank;
|
private Spinner mSpinnerRank;
|
||||||
private LinearLayout mLayoutApprovals;
|
private LinearLayout mLayoutApprovals;
|
||||||
|
private EditText mEditProfileName;
|
||||||
|
private EditText mEditProfileAge;
|
||||||
|
private LinearLayout mLayoutItems;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
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);
|
mEditNumber = (EditText) view.findViewById(R.id.number);
|
||||||
mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
|
mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
|
||||||
mLayoutApprovals = (LinearLayout) view.findViewById(R.id.approvals);
|
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
|
@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
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
switch (parent.getId()) {
|
switch (parent.getId()) {
|
||||||
@@ -171,9 +227,42 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
// Nothing to do
|
// 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
|
* Loads the restrictions for the AppRestrictionSchema sample.
|
||||||
* read the default value for the "can_say_hello" restriction.
|
|
||||||
*
|
*
|
||||||
* @param activity The activity
|
* @param activity The activity
|
||||||
*/
|
*/
|
||||||
@@ -203,6 +292,28 @@ public class AppRestrictionEnforcerFragment extends Fragment implements
|
|||||||
TextUtils.join(DELIMETER,
|
TextUtils.join(DELIMETER,
|
||||||
restriction.getAllSelectedStrings())),
|
restriction.getAllSelectedStrings())),
|
||||||
DELIMETER));
|
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.
|
* 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();
|
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) {
|
private void saveRestrictions(Activity activity) {
|
||||||
DevicePolicyManager devicePolicyManager
|
DevicePolicyManager devicePolicyManager
|
||||||
= (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
|
= (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"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
tools:text="@string/your_rank"/>
|
tools:text="@string/your_rank"/>
|
||||||
|
|
||||||
<include layout="@layout/separator"/>
|
<include layout="@layout/separator" android:id="@+id/bundle_separator"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/approvals_you_have"
|
android:id="@+id/approvals_you_have"
|
||||||
@@ -68,6 +68,24 @@ limitations under the License.
|
|||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
tools:text="@string/approvals_you_have"/>
|
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>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</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="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>
|
<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>
|
</resources>
|
||||||
|
|||||||
@@ -25,5 +25,7 @@ limitations under the License.
|
|||||||
<string name="your_rank">Your rank: %s</string>
|
<string name="your_rank">Your rank: %s</string>
|
||||||
<string name="approvals_you_have">Approvals you have: %s</string>
|
<string name="approvals_you_have">Approvals you have: %s</string>
|
||||||
<string name="none">none</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>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<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
|
Copyright 2014 The Android Open Source Project
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
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
|
https://developer.android.com/reference/android/content/RestrictionsManager.html
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- Boolean restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@bool/default_can_say_hello"
|
android:defaultValue="@bool/default_can_say_hello"
|
||||||
android:description="@string/description_can_say_hello"
|
android:description="@string/description_can_say_hello"
|
||||||
@@ -28,6 +28,7 @@ limitations under the License.
|
|||||||
android:restrictionType="bool"
|
android:restrictionType="bool"
|
||||||
android:title="@string/title_can_say_hello"/>
|
android:title="@string/title_can_say_hello"/>
|
||||||
|
|
||||||
|
<!-- String restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@string/default_message"
|
android:defaultValue="@string/default_message"
|
||||||
android:description="@string/description_message"
|
android:description="@string/description_message"
|
||||||
@@ -35,6 +36,7 @@ limitations under the License.
|
|||||||
android:restrictionType="string"
|
android:restrictionType="string"
|
||||||
android:title="@string/title_message"/>
|
android:title="@string/title_message"/>
|
||||||
|
|
||||||
|
<!-- Integer restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@integer/default_number"
|
android:defaultValue="@integer/default_number"
|
||||||
android:description="@string/description_number"
|
android:description="@string/description_number"
|
||||||
@@ -42,6 +44,7 @@ limitations under the License.
|
|||||||
android:restrictionType="integer"
|
android:restrictionType="integer"
|
||||||
android:title="@string/title_number"/>
|
android:title="@string/title_number"/>
|
||||||
|
|
||||||
|
<!-- Choice restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@string/default_rank"
|
android:defaultValue="@string/default_rank"
|
||||||
android:description="@string/description_rank"
|
android:description="@string/description_rank"
|
||||||
@@ -51,6 +54,7 @@ limitations under the License.
|
|||||||
android:restrictionType="choice"
|
android:restrictionType="choice"
|
||||||
android:title="@string/title_rank"/>
|
android:title="@string/title_rank"/>
|
||||||
|
|
||||||
|
<!-- Multi-select restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@array/default_approvals"
|
android:defaultValue="@array/default_approvals"
|
||||||
android:description="@string/description_approvals"
|
android:description="@string/description_approvals"
|
||||||
@@ -60,6 +64,7 @@ limitations under the License.
|
|||||||
android:restrictionType="multi-select"
|
android:restrictionType="multi-select"
|
||||||
android:title="@string/title_approvals"/>
|
android:title="@string/title_approvals"/>
|
||||||
|
|
||||||
|
<!-- Hidden restriction -->
|
||||||
<restriction
|
<restriction
|
||||||
android:defaultValue="@string/default_secret_code"
|
android:defaultValue="@string/default_secret_code"
|
||||||
android:description="@string/description_secret_code"
|
android:description="@string/description_secret_code"
|
||||||
@@ -67,4 +72,46 @@ limitations under the License.
|
|||||||
android:restrictionType="hidden"
|
android:restrictionType="hidden"
|
||||||
android:title="@string/title_secret_code"/>
|
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>
|
</restrictions>
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ package com.example.android.apprestrictionschema;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.RestrictionEntry;
|
import android.content.RestrictionEntry;
|
||||||
import android.content.RestrictionsManager;
|
import android.content.RestrictionsManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.text.TextUtils;
|
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_NUMBER = "number";
|
||||||
private static final String KEY_RANK = "rank";
|
private static final String KEY_RANK = "rank";
|
||||||
private static final String KEY_APPROVALS = "approvals";
|
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)
|
// Message to show when the button is clicked (String restriction)
|
||||||
private String mMessage;
|
private String mMessage;
|
||||||
@@ -59,6 +69,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
private TextView mTextNumber;
|
private TextView mTextNumber;
|
||||||
private TextView mTextRank;
|
private TextView mTextRank;
|
||||||
private TextView mTextApprovals;
|
private TextView mTextApprovals;
|
||||||
|
private TextView mTextProfile;
|
||||||
|
private TextView mTextItems;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
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);
|
mTextNumber = (TextView) view.findViewById(R.id.your_number);
|
||||||
mTextRank = (TextView) view.findViewById(R.id.your_rank);
|
mTextRank = (TextView) view.findViewById(R.id.your_rank);
|
||||||
mTextApprovals = (TextView) view.findViewById(R.id.approvals_you_have);
|
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);
|
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
|
@Override
|
||||||
@@ -86,7 +113,8 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
RestrictionsManager manager =
|
RestrictionsManager manager =
|
||||||
(RestrictionsManager) getActivity().getSystemService(Context.RESTRICTIONS_SERVICE);
|
(RestrictionsManager) getActivity().getSystemService(Context.RESTRICTIONS_SERVICE);
|
||||||
Bundle restrictions = manager.getApplicationRestrictions();
|
Bundle restrictions = manager.getApplicationRestrictions();
|
||||||
List<RestrictionEntry> entries = manager.getManifestRestrictions(getActivity().getApplicationContext().getPackageName());
|
List<RestrictionEntry> entries = manager.getManifestRestrictions(
|
||||||
|
getActivity().getApplicationContext().getPackageName());
|
||||||
for (RestrictionEntry entry : entries) {
|
for (RestrictionEntry entry : entries) {
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
Log.d(TAG, "key: " + key);
|
Log.d(TAG, "key: " + key);
|
||||||
@@ -100,6 +128,10 @@ public class AppRestrictionSchemaFragment extends Fragment implements View.OnCli
|
|||||||
updateRank(entry, restrictions);
|
updateRank(entry, restrictions);
|
||||||
} else if (key.equals(KEY_APPROVALS)) {
|
} else if (key.equals(KEY_APPROVALS)) {
|
||||||
updateApprovals(entry, restrictions);
|
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));
|
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
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
switch (view.getId()) {
|
switch (view.getId()) {
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="@dimen/margin_medium">
|
android:padding="@dimen/margin_medium"
|
||||||
|
>
|
||||||
|
|
||||||
<Button android:id="@+id/button_open_usage_setting"
|
<Button android:id="@+id/button_open_usage_setting"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -50,6 +52,8 @@
|
|||||||
android:scrollbars="vertical"
|
android:scrollbars="vertical"
|
||||||
android:drawSelectorOnTop="true"
|
android:drawSelectorOnTop="true"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"/>
|
android:layout_height="match_parent"
|
||||||
|
app:layoutManager="LinearLayoutManager"
|
||||||
|
/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -17,6 +17,6 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light" />
|
<style name="Theme.Base" parent="android:Theme.Holo.Light" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- Activity themes -->
|
||||||
<style name="Theme.Base" parent="@style/Theme.AppCompat.Light">
|
<style name="Theme.Base" parent="android:Theme.Material.Light">
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!-- Activity themes -->
|
<!-- 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" />
|
<style name="Theme.Sample" parent="Theme.Base" />
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -91,10 +90,9 @@ public class AppUsageStatisticsFragment extends Fragment {
|
|||||||
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
public void onViewCreated(View rootView, Bundle savedInstanceState) {
|
||||||
super.onViewCreated(rootView, savedInstanceState);
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
|
||||||
mLayoutManager = new LinearLayoutManager(getActivity());
|
|
||||||
mUsageListAdapter = new UsageListAdapter();
|
mUsageListAdapter = new UsageListAdapter();
|
||||||
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_app_usage);
|
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview_app_usage);
|
||||||
mRecyclerView.setLayoutManager(mLayoutManager);
|
mLayoutManager = mRecyclerView.getLayoutManager();
|
||||||
mRecyclerView.scrollToPosition(0);
|
mRecyclerView.scrollToPosition(0);
|
||||||
mRecyclerView.setAdapter(mUsageListAdapter);
|
mRecyclerView.setAdapter(mUsageListAdapter);
|
||||||
mOpenUsageSettingButton = (Button) rootView.findViewById(R.id.button_open_usage_setting);
|
mOpenUsageSettingButton = (Button) rootView.findViewById(R.id.button_open_usage_setting);
|
||||||
@@ -197,7 +195,7 @@ public class AppUsageStatisticsFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compare(UsageStats left, UsageStats right) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||