Merged PR 11519: GraphNotifications SDK Samples
GraphNotifications/UserNotifications Samples for the following Platforms - Android - iOS - Windows
|
@ -0,0 +1,9 @@
|
||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea/workspace.xml
|
||||||
|
/.idea/libraries
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
|
@ -0,0 +1,48 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 27
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.microsoft.connecteddevices.graphnotifications"
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 27
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packagingOptions {
|
||||||
|
pickFirst 'lib/x86/libc++_shared.so'
|
||||||
|
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||||
|
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||||
|
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||||
|
implementation 'com.android.support:design:27.1.1'
|
||||||
|
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
||||||
|
implementation 'com.google.firebase:firebase-messaging:17.1.0'
|
||||||
|
implementation 'com.google.firebase:firebase-core:16.0.1'
|
||||||
|
|
||||||
|
testImplementation 'junit:junit:4.12'
|
||||||
|
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||||
|
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||||
|
|
||||||
|
// Local version of SDK, since we don't have public maven (yet)
|
||||||
|
implementation(name: 'connecteddevices-sdk', ext: 'aar')
|
||||||
|
|
||||||
|
// For login
|
||||||
|
implementation project(path: ':sampleaccountproviders')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "782586391244",
|
||||||
|
"firebase_url": "https://graphnotifications-a61a7.firebaseio.com",
|
||||||
|
"project_id": "graphnotifications-a61a7",
|
||||||
|
"storage_bucket": "graphnotifications-a61a7.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:782586391244:android:7bd1ae34fbb0ccaa",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.microsoft.connecteddevices.graphnotifications"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "782586391244-93dahs8garncusah95oene5gk1sj88uh.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyCsYVrZK8DVYTTIP_YdT-5NCuRY5sIkmqM"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"analytics_service": {
|
||||||
|
"status": 1
|
||||||
|
},
|
||||||
|
"appinvite_service": {
|
||||||
|
"status": 1,
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
},
|
||||||
|
"ads_service": {
|
||||||
|
"status": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.microsoft.connecteddevices.graphnotifications", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="com.microsoft.connecteddevices.graphnotifications">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/AppTheme.NoActionBar">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<service android:name=".FCMListenerService" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) Microsoft Corporation. All rights reserved.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.android.gms.tasks.OnCompleteListener;
|
||||||
|
import com.google.android.gms.tasks.Task;
|
||||||
|
import com.google.firebase.iid.FirebaseInstanceId;
|
||||||
|
import com.google.firebase.iid.InstanceIdResult;
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||||
|
import com.google.firebase.messaging.RemoteMessage;
|
||||||
|
import com.microsoft.connecteddevices.core.NotificationReceiver;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communicates with Firebase Cloud Messaging.
|
||||||
|
*/
|
||||||
|
public class FCMListenerService extends FirebaseMessagingService {
|
||||||
|
private static final String TAG = "FCMListenerService";
|
||||||
|
|
||||||
|
private static final String RegistrationComplete = "registrationComplete";
|
||||||
|
private static final String TOKEN = "TOKEN";
|
||||||
|
|
||||||
|
private static String s_previousToken = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
|
||||||
|
@Override
|
||||||
|
public void onComplete(@NonNull Task<InstanceIdResult> task) {
|
||||||
|
if (task.isSuccessful()) {
|
||||||
|
String token = task.getResult().getToken();
|
||||||
|
if (!token.isEmpty()) {
|
||||||
|
FCMListenerService.this.onNewToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether it's a rome notification or not.
|
||||||
|
* If it is a rome notification,
|
||||||
|
* It will notify the apps with the information in the notification.
|
||||||
|
* @param message FCM class for messaging with a from a data field.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onMessageReceived(RemoteMessage message) {
|
||||||
|
Log.d(TAG, "From: " + message.getFrom());
|
||||||
|
Map data = message.getData();
|
||||||
|
if (!NotificationReceiver.Receive(data)) {
|
||||||
|
Log.d(TAG, "FCM client received a message that was not a Rome notification");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNewToken(String token) {
|
||||||
|
if (token != null && !token.equals(s_previousToken)) {
|
||||||
|
s_previousToken = token;
|
||||||
|
Intent registrationComplete = new Intent(RegistrationComplete);
|
||||||
|
registrationComplete.putExtra(TOKEN, token);
|
||||||
|
LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,518 @@
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.design.widget.TabLayout;
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
|
import android.support.v4.app.FragmentPagerAdapter;
|
||||||
|
import android.support.v4.view.ViewPager;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.support.v7.widget.Toolbar;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
import com.microsoft.connecteddevices.base.EventListener;
|
||||||
|
import com.microsoft.connecteddevices.core.Platform;
|
||||||
|
import com.microsoft.connecteddevices.sampleaccountproviders.AADMSAAccountProvider;
|
||||||
|
import com.microsoft.connecteddevices.userdata.UserDataFeed;
|
||||||
|
import com.microsoft.connecteddevices.userdata.UserDataFeedSyncScope;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotification;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationChannel;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationReadState;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationReader;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationReaderOptions;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationStatus;
|
||||||
|
import com.microsoft.connecteddevices.usernotifications.UserNotificationUserActionState;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link android.support.v4.view.PagerAdapter} that will provide
|
||||||
|
* fragments for each of the sections. We use a
|
||||||
|
* {@link FragmentPagerAdapter} derivative, which will keep every
|
||||||
|
* loaded fragment in memory. If this becomes too memory intensive, it
|
||||||
|
* may be best to switch to a
|
||||||
|
* {@link android.support.v4.app.FragmentStatePagerAdapter}.
|
||||||
|
*/
|
||||||
|
private SectionsPagerAdapter mSectionsPagerAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link ViewPager} that will host the section contents.
|
||||||
|
*/
|
||||||
|
private ViewPager mViewPager;
|
||||||
|
|
||||||
|
private static Platform sPlatform;
|
||||||
|
private static AADMSAAccountProvider sAccountProvider;
|
||||||
|
private static UserNotificationReader sReader;
|
||||||
|
private static ArrayList<UserNotification> sNewNotifications = new ArrayList<>();
|
||||||
|
private static final ArrayList<UserNotification> sHistoricalNotifications = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
// Create the adapter that will return a fragment for each of the three
|
||||||
|
// primary sections of the activity.
|
||||||
|
mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
|
||||||
|
|
||||||
|
// Set up the ViewPager with the sections adapter.
|
||||||
|
mViewPager = findViewById(R.id.container);
|
||||||
|
mViewPager.setAdapter(mSectionsPagerAdapter);
|
||||||
|
|
||||||
|
TabLayout tabLayout = findViewById(R.id.tabs);
|
||||||
|
|
||||||
|
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
|
||||||
|
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager));
|
||||||
|
|
||||||
|
ensurePlatformInitialized(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
// Inflate the menu; this adds items to the action bar if it is present.
|
||||||
|
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
// Handle action bar item clicks here. The action bar will
|
||||||
|
// automatically handle clicks on the Home/Up button, so long
|
||||||
|
// as you specify a parent activity in AndroidManifest.xml.
|
||||||
|
int id = item.getItemId();
|
||||||
|
|
||||||
|
//noinspection SimplifiableIfStatement
|
||||||
|
if (id == R.id.action_settings) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RunnableManager {
|
||||||
|
private static Runnable sNewNotificationsUpdated;
|
||||||
|
private static Runnable sHistoryUpdated;
|
||||||
|
|
||||||
|
static void setNewNotificationsUpdated(Runnable runnable) {
|
||||||
|
sNewNotificationsUpdated = runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setHistoryUpdated(Runnable runnable) {
|
||||||
|
sHistoryUpdated = runnable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Runnable getNewNotificationsUpdated() {
|
||||||
|
return sNewNotificationsUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Runnable getHistoryUpdated() {
|
||||||
|
return sHistoryUpdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LoginFragment extends Fragment {
|
||||||
|
private Button mAadButton;
|
||||||
|
private Button mMsaButton;
|
||||||
|
boolean firstCreate = true;
|
||||||
|
|
||||||
|
public LoginFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LoginFragment newInstance() {
|
||||||
|
return new LoginFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
|
||||||
|
mAadButton = rootView.findViewById(R.id.login_aad_button);
|
||||||
|
mMsaButton = rootView.findViewById(R.id.login_msa_button);
|
||||||
|
setState(sAccountProvider.getSignInState());
|
||||||
|
if (firstCreate && sAccountProvider.getSignInState() != AADMSAAccountProvider.State.SignedOut) {
|
||||||
|
firstCreate = false;
|
||||||
|
MainActivity.setupChannel(getActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setState(AADMSAAccountProvider.State loginState) {
|
||||||
|
switch (loginState) {
|
||||||
|
case SignedOut:
|
||||||
|
mAadButton.setEnabled(true);
|
||||||
|
mAadButton.setText(R.string.login_aad);
|
||||||
|
mAadButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
sAccountProvider.signInAAD().whenComplete(new AsyncOperation.ResultBiConsumer<Boolean, Throwable>() {
|
||||||
|
@Override
|
||||||
|
public void accept(Boolean success, Throwable throwable) throws Throwable {
|
||||||
|
if (throwable == null && success) {
|
||||||
|
setState(sAccountProvider.getSignInState());
|
||||||
|
MainActivity.setupChannel(getActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mMsaButton.setEnabled(true);
|
||||||
|
mMsaButton.setText(R.string.login_msa);
|
||||||
|
mMsaButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
sAccountProvider.signInMSA(getActivity()).whenComplete(new AsyncOperation.ResultBiConsumer<Boolean, Throwable>() {
|
||||||
|
@Override
|
||||||
|
public void accept(Boolean success, Throwable throwable) throws Throwable {
|
||||||
|
if (throwable == null && success) {
|
||||||
|
setState(sAccountProvider.getSignInState());
|
||||||
|
MainActivity.setupChannel(getActivity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SignedInAAD:
|
||||||
|
mAadButton.setText(R.string.logout);
|
||||||
|
mAadButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
sAccountProvider.signOutAAD();
|
||||||
|
setState(sAccountProvider.getSignInState());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mMsaButton.setEnabled(false);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SignedInMSA:
|
||||||
|
mAadButton.setEnabled(false);
|
||||||
|
mMsaButton.setText(R.string.logout);
|
||||||
|
mMsaButton.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
sAccountProvider.signOutMSA(getActivity());
|
||||||
|
setState(sAccountProvider.getSignInState());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class NotificationArrayAdapter extends ArrayAdapter<UserNotification> {
|
||||||
|
NotificationArrayAdapter(Context context, List<UserNotification> items) {
|
||||||
|
super(context, R.layout.notifications_list_item, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
final UserNotification notification = sNewNotifications.get(position);
|
||||||
|
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = LayoutInflater.from(getContext()).inflate(R.layout.notifications_list_item, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView idView = convertView.findViewById(R.id.notification_id);
|
||||||
|
idView.setText(notification.getId());
|
||||||
|
|
||||||
|
TextView textView = convertView.findViewById(R.id.notification_text);
|
||||||
|
String content = notification.getContent();
|
||||||
|
textView.setText(content);
|
||||||
|
|
||||||
|
convertView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
notification.setUserActionState(UserNotificationUserActionState.DISMISSED);
|
||||||
|
notification.saveAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NotificationsFragment extends Fragment {
|
||||||
|
NotificationArrayAdapter mNotificationArrayAdapter;
|
||||||
|
public NotificationsFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of this fragment for the given section
|
||||||
|
* number.
|
||||||
|
*/
|
||||||
|
public static NotificationsFragment newInstance() {
|
||||||
|
return new NotificationsFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
mNotificationArrayAdapter = new NotificationArrayAdapter(getContext(), sNewNotifications);
|
||||||
|
RunnableManager.setNewNotificationsUpdated(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(getContext(), "Got a new notification!", Toast.LENGTH_SHORT).show();
|
||||||
|
mNotificationArrayAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_notifications, container, false);
|
||||||
|
ListView listView = rootView.findViewById(R.id.notificationListView);
|
||||||
|
listView.setAdapter(mNotificationArrayAdapter);
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class HistoryArrayAdapter extends ArrayAdapter<UserNotification> {
|
||||||
|
HistoryArrayAdapter(Context context, List<UserNotification> items) {
|
||||||
|
super(context, R.layout.notifications_list_item, items);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
final UserNotification notification = sHistoricalNotifications.get(position);
|
||||||
|
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = LayoutInflater.from(getContext()).inflate(R.layout.notifications_list_item, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView idView = convertView.findViewById(R.id.notification_id);
|
||||||
|
idView.setText(notification.getId());
|
||||||
|
|
||||||
|
TextView textView = convertView.findViewById(R.id.notification_text);
|
||||||
|
textView.setText(notification.getContent());
|
||||||
|
|
||||||
|
convertView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View view) {
|
||||||
|
notification.setReadState(UserNotificationReadState.READ);
|
||||||
|
notification.saveAsync();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HistoryFragment extends Fragment {
|
||||||
|
private HistoryArrayAdapter mHistoryArrayAdapter;
|
||||||
|
public HistoryFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of this fragment for the given section
|
||||||
|
* number.
|
||||||
|
*/
|
||||||
|
public static HistoryFragment newInstance() {
|
||||||
|
return new HistoryFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
View rootView = inflater.inflate(R.layout.fragment_history, container, false);
|
||||||
|
mHistoryArrayAdapter = new HistoryArrayAdapter(getContext(), sHistoricalNotifications);
|
||||||
|
RunnableManager.setHistoryUpdated(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mHistoryArrayAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ListView listView = rootView.findViewById(R.id.historyListView);
|
||||||
|
listView.setAdapter(mHistoryArrayAdapter);
|
||||||
|
return rootView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
|
||||||
|
* one of the sections/tabs/pages.
|
||||||
|
*/
|
||||||
|
private class SectionsPagerAdapter extends FragmentPagerAdapter {
|
||||||
|
LoginFragment mLoginFragment;
|
||||||
|
NotificationsFragment mNotificationFragment;
|
||||||
|
HistoryFragment mHistoryFragment;
|
||||||
|
|
||||||
|
SectionsPagerAdapter(FragmentManager fm) {
|
||||||
|
super(fm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
switch(position)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
if (mLoginFragment == null) {
|
||||||
|
mLoginFragment = LoginFragment.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mLoginFragment;
|
||||||
|
case 1:
|
||||||
|
if (mNotificationFragment == null) {
|
||||||
|
mNotificationFragment = NotificationsFragment.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mNotificationFragment;
|
||||||
|
case 2:
|
||||||
|
if (mHistoryFragment == null) {
|
||||||
|
mHistoryFragment = HistoryFragment.newInstance();
|
||||||
|
}
|
||||||
|
|
||||||
|
return mHistoryFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
// Show 3 total pages.
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static synchronized Platform ensurePlatformInitialized(Context context) {
|
||||||
|
// First see if we have an existing platform
|
||||||
|
if (sPlatform != null) {
|
||||||
|
return sPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No existing platform, so we have to create our own
|
||||||
|
RomeNotificationProvider notificationProvider = new RomeNotificationProvider(context);
|
||||||
|
final Map<String, String[]> msaScopeOverrides = new ArrayMap<>();
|
||||||
|
msaScopeOverrides.put("https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
|
||||||
|
new String[] { "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
|
||||||
|
"https://activity.windows.com/Notifications.ReadWrite.CreatedByApp"});
|
||||||
|
sAccountProvider = new AADMSAAccountProvider(Secrets.MSA_CLIENT_ID, msaScopeOverrides, Secrets.AAD_CLIENT_ID, Secrets.AAD_REDIRECT_URI, context);
|
||||||
|
sPlatform = new Platform(context, sAccountProvider, notificationProvider);
|
||||||
|
return sPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setupChannel(final Activity activity) {
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (sAccountProvider.getUserAccounts().length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDataFeed dataFeed = UserDataFeed.getForAccount(sAccountProvider.getUserAccounts()[0], sPlatform, Secrets.APP_HOST_NAME);
|
||||||
|
dataFeed.addSyncScopes(new UserDataFeedSyncScope[]{UserNotificationChannel.getSyncScope()});
|
||||||
|
UserNotificationChannel channel = new UserNotificationChannel(dataFeed);
|
||||||
|
UserNotificationReaderOptions options = new UserNotificationReaderOptions();
|
||||||
|
sReader = channel.createReaderWithOptions(options);
|
||||||
|
sReader.readBatchAsync(Long.MAX_VALUE).thenAccept(new AsyncOperation.ResultConsumer<UserNotification[]>() {
|
||||||
|
@Override
|
||||||
|
public void accept(UserNotification[] userNotifications) throws Throwable {
|
||||||
|
synchronized (sHistoricalNotifications) {
|
||||||
|
for (UserNotification notification : userNotifications) {
|
||||||
|
if (notification.getReadState() == UserNotificationReadState.UNREAD) {
|
||||||
|
sHistoricalNotifications.add(notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RunnableManager.getHistoryUpdated() != null) {
|
||||||
|
activity.runOnUiThread(RunnableManager.getHistoryUpdated());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sReader.addDataChangedListener(new EventListener<UserNotificationReader, Void>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(UserNotificationReader userNotificationReader, Void aVoid) {
|
||||||
|
userNotificationReader.readBatchAsync(Long.MAX_VALUE).thenAccept(new AsyncOperation.ResultConsumer<UserNotification[]>() {
|
||||||
|
@Override
|
||||||
|
public void accept(UserNotification[] userNotifications) throws Throwable {
|
||||||
|
boolean updatedNew = false;
|
||||||
|
boolean updatedHistorical = false;
|
||||||
|
synchronized (sHistoricalNotifications) {
|
||||||
|
for (final UserNotification notification : userNotifications) {
|
||||||
|
if (notification.getStatus() == UserNotificationStatus.ACTIVE && notification.getReadState() == UserNotificationReadState.UNREAD) {
|
||||||
|
switch (notification.getUserActionState()) {
|
||||||
|
case NO_INTERACTION:
|
||||||
|
// Brand new notification
|
||||||
|
for (int i = 0; i < sNewNotifications.size(); i++) {
|
||||||
|
if (sNewNotifications.get(i).getId().equals(notification.getId())) {
|
||||||
|
sNewNotifications.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sNewNotifications.add(notification);
|
||||||
|
updatedNew = true;
|
||||||
|
break;
|
||||||
|
case DISMISSED:
|
||||||
|
// Existing notification we dismissed, move from new -> history
|
||||||
|
for (int i = 0; i < sNewNotifications.size(); i++) {
|
||||||
|
if (sNewNotifications.get(i).getId().equals(notification.getId())) {
|
||||||
|
sNewNotifications.remove(i);
|
||||||
|
updatedNew = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < sHistoricalNotifications.size(); i++) {
|
||||||
|
if (sHistoricalNotifications.get(i).getId().equals(notification.getId())) {
|
||||||
|
sHistoricalNotifications.remove(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sHistoricalNotifications.add(notification);
|
||||||
|
updatedHistorical = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Something unexpected happened, just ignore for future flexibility
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// historical item has been updated, should only happen if marked as read
|
||||||
|
for (int i = 0; i < sHistoricalNotifications.size(); i++) {
|
||||||
|
if (sHistoricalNotifications.get(i).getId().equals(notification.getId())) {
|
||||||
|
sHistoricalNotifications.remove(i);
|
||||||
|
updatedHistorical = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedNew && RunnableManager.getNewNotificationsUpdated() != null) {
|
||||||
|
activity.runOnUiThread(RunnableManager.getNewNotificationsUpdated());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedHistorical && RunnableManager.getHistoryUpdated() != null) {
|
||||||
|
activity.runOnUiThread(RunnableManager.getHistoryUpdated());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.support.v4.content.LocalBroadcastManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
import com.microsoft.connecteddevices.base.EventListener;
|
||||||
|
import com.microsoft.connecteddevices.core.NotificationProvider;
|
||||||
|
import com.microsoft.connecteddevices.core.NotificationRegistration;
|
||||||
|
import com.microsoft.connecteddevices.core.NotificationType;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class RomeNotificationProvider extends BroadcastReceiver implements NotificationProvider {
|
||||||
|
private Map<Long, EventListener<NotificationProvider, NotificationRegistration>> mListenerMap;
|
||||||
|
private Long mNextListenerId = 0L;
|
||||||
|
private NotificationRegistration mNotificationRegistration;
|
||||||
|
private AsyncOperation<NotificationRegistration> mAsync;
|
||||||
|
private Context mContext;
|
||||||
|
private static final String RegistrationComplete = "registrationComplete";
|
||||||
|
|
||||||
|
RomeNotificationProvider(Context context) {
|
||||||
|
mListenerMap = new HashMap<>();
|
||||||
|
mContext = context;
|
||||||
|
|
||||||
|
registerFCMBroadcastReceiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns Notification Registration after it completes async operation.
|
||||||
|
* @return Notification Registration.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized AsyncOperation<NotificationRegistration> getNotificationRegistrationAsync() {
|
||||||
|
if (mAsync == null) {
|
||||||
|
mAsync = new AsyncOperation<>();
|
||||||
|
}
|
||||||
|
if (mNotificationRegistration != null) {
|
||||||
|
mAsync.complete(mNotificationRegistration);
|
||||||
|
}
|
||||||
|
return mAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function adds new event listener to notification provider.
|
||||||
|
* @param listener the EventListener.
|
||||||
|
* @return id next event listener id.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized long addNotificationProviderChangedListener(
|
||||||
|
EventListener<NotificationProvider, NotificationRegistration> listener) {
|
||||||
|
mListenerMap.put(mNextListenerId, listener);
|
||||||
|
return mNextListenerId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function removes the event listener.
|
||||||
|
* @param id the id corresponds to the event listener that would be removed.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized void removeNotificationProviderChangedListener(long id) {
|
||||||
|
mListenerMap.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When FCM has been registered, this will get fired.
|
||||||
|
* @param context the application's context.
|
||||||
|
* @param intent the broadcast intent sent to any interested BroadcastReceiver components.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
String token = null;
|
||||||
|
String action = intent.getAction();
|
||||||
|
|
||||||
|
Log.i("Receiver", "Broadcast received: " + action);
|
||||||
|
|
||||||
|
if (action.equals(RegistrationComplete)) {
|
||||||
|
token = intent.getExtras().getString("TOKEN");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
Log.e("GraphNotifications",
|
||||||
|
"Got notification that FCM had been registered, but token is null. Was app ID set in FCMRegistrationIntentService?");
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
mNotificationRegistration = new NotificationRegistration(NotificationType.FCM, token, Secrets.FCM_SENDER_ID, "GraphNotifications");
|
||||||
|
|
||||||
|
if (mAsync == null) {
|
||||||
|
mAsync = new AsyncOperation<>();
|
||||||
|
}
|
||||||
|
mAsync.complete(mNotificationRegistration);
|
||||||
|
mAsync = new AsyncOperation<>();
|
||||||
|
|
||||||
|
for (EventListener<NotificationProvider, NotificationRegistration> event : mListenerMap.values()) {
|
||||||
|
event.onEvent(this, mNotificationRegistration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This function is called to start FCM registration service.
|
||||||
|
* Start FCMRegistrationIntentService to register with FCM.
|
||||||
|
*/
|
||||||
|
private void startService() {
|
||||||
|
Intent registrationIntentService = new Intent(mContext, FCMListenerService.class);
|
||||||
|
mContext.startService(registrationIntentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called to register FCM.
|
||||||
|
*/
|
||||||
|
private void registerFCMBroadcastReceiver() {
|
||||||
|
LocalBroadcastManager.getInstance(mContext).registerReceiver(this, new IntentFilter(RegistrationComplete));
|
||||||
|
startService();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
class Secrets {
|
||||||
|
// These come from the converged app registration portal at apps.dev.microsoft.com
|
||||||
|
// MSA_CLIENT_ID: Id of this app's registration in the MSA portal
|
||||||
|
// AAD_CLIENT_ID: Id of this app's registration in the Azure portal
|
||||||
|
// AAD_REDIRECT_URI: A Uri that this app is registered with in the Azure portal.
|
||||||
|
// AAD is supposed to use this Uri to call the app back after login (currently not true, external requirement)
|
||||||
|
// And this app is supposed to be able to handle this Uri (currently not true)
|
||||||
|
// APP_HOST_NAME Cross-device domain of this app's registration
|
||||||
|
static final String MSA_CLIENT_ID = "<<MSA client ID goes here>>";
|
||||||
|
static final String AAD_CLIENT_ID = "<<AAD client ID goes here>>";
|
||||||
|
static final String AAD_REDIRECT_URI = "<<AAD redirect URI goes here>>";
|
||||||
|
static final String APP_HOST_NAME = "<<App cross-device domain goes here>>";
|
||||||
|
|
||||||
|
// Your client's Firebase Cloud Messaging Sender Id
|
||||||
|
static final String FCM_SENDER_ID = "<<FCM sender ID goes here>>";
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,170 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/main_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
tools:context="com.microsoft.connecteddevices.graphnotifications.MainActivity">
|
||||||
|
|
||||||
|
<android.support.design.widget.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="@dimen/appbar_padding_top"
|
||||||
|
android:theme="@style/AppTheme.AppBarOverlay">
|
||||||
|
|
||||||
|
<android.support.v7.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
|
app:popupTheme="@style/AppTheme.PopupOverlay"
|
||||||
|
app:title="@string/app_name">
|
||||||
|
|
||||||
|
</android.support.v7.widget.Toolbar>
|
||||||
|
|
||||||
|
<android.support.design.widget.TabLayout
|
||||||
|
android:id="@+id/tabs"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<android.support.design.widget.TabItem
|
||||||
|
android:id="@+id/tabItem"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/tab_text_1" />
|
||||||
|
|
||||||
|
<android.support.design.widget.TabItem
|
||||||
|
android:id="@+id/tabItem2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/tab_text_2" />
|
||||||
|
|
||||||
|
<android.support.design.widget.TabItem
|
||||||
|
android:id="@+id/tabItem3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/tab_text_3" />
|
||||||
|
|
||||||
|
</android.support.design.widget.TabLayout>
|
||||||
|
</android.support.design.widget.AppBarLayout>
|
||||||
|
|
||||||
|
<android.support.v4.view.ViewPager
|
||||||
|
android:id="@+id/container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?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" >
|
||||||
|
<WebView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/webv"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/fragment_history"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/historyListView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/constraintLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="com.microsoft.connecteddevices.graphnotifications.MainActivity$LoginFragment">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/login_aad_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/login_aad"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.323" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/login_msa_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="160dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/login_msa"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.502"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/login_aad_button"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/fragment_usernotification"
|
||||||
|
android:orientation="vertical" android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/notificationListView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="30dp" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_id"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:text="" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notification_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_below="@+id/notification_id"
|
||||||
|
android:text="" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context="com.microsoft.connecteddevices.graphnotifications..MainActivity">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:orderInCategory="100"
|
||||||
|
android:title="@string/action_settings"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
После Ширина: | Высота: | Размер: 3.0 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
После Ширина: | Высота: | Размер: 4.9 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
После Ширина: | Высота: | Размер: 2.0 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
После Ширина: | Высота: | Размер: 2.8 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
После Ширина: | Высота: | Размер: 4.5 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
После Ширина: | Высота: | Размер: 6.9 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
После Ширина: | Высота: | Размер: 6.3 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
После Ширина: | Высота: | Размер: 10 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
После Ширина: | Высота: | Размер: 9.0 KiB |
Двоичные данные
Android/samples/graphnotificationssample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
После Ширина: | Высота: | Размер: 15 KiB |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
</resources>
|
|
@ -0,0 +1,7 @@
|
||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
<dimen name="fab_margin">16dp</dimen>
|
||||||
|
<dimen name="appbar_padding_top">8dp</dimen>
|
||||||
|
</resources>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Graph Notifications</string>
|
||||||
|
<string name="tab_text_1">Tab 1</string>
|
||||||
|
<string name="tab_text_2">Tab 2</string>
|
||||||
|
<string name="tab_text_3">Tab 3</string>
|
||||||
|
<string name="action_settings">Settings</string>
|
||||||
|
<string name="section_format">Hello World from section: %1$d</string>
|
||||||
|
<string name="login_aad">Login with Work/School Account</string>
|
||||||
|
<string name="login_msa">Login with Personal Account</string>
|
||||||
|
<string name="logout">Log Out</string>
|
||||||
|
</resources>
|
|
@ -0,0 +1,20 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
|
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||||
|
|
||||||
|
</resources>
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:3.1.4'
|
||||||
|
classpath 'com.google.gms:google-services:4.0.2'
|
||||||
|
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
jcenter()
|
||||||
|
mavenCentral()
|
||||||
|
flatDir {
|
||||||
|
dirs 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
Двоичные данные
Android/samples/graphnotificationssample/gradle/wrapper/gradle-wrapper.jar
поставляемый
Normal file
6
Android/samples/graphnotificationssample/gradle/wrapper/gradle-wrapper.properties
поставляемый
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Mon Aug 13 09:22:18 PDT 2018
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS=""
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn ( ) {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die ( ) {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin, switch paths to Windows format before running java
|
||||||
|
if $cygwin ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
(0) set -- ;;
|
||||||
|
(1) set -- "$args0" ;;
|
||||||
|
(2) set -- "$args0" "$args1" ;;
|
||||||
|
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||||
|
function splitJvmOpts() {
|
||||||
|
JVM_OPTS=("$@")
|
||||||
|
}
|
||||||
|
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||||
|
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
|
@ -0,0 +1,90 @@
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS=
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto init
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:init
|
||||||
|
@rem Get command-line arguments, handling Windowz variants
|
||||||
|
|
||||||
|
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||||
|
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||||
|
|
||||||
|
:win9xME_args
|
||||||
|
@rem Slurp the command line arguments.
|
||||||
|
set CMD_LINE_ARGS=
|
||||||
|
set _SKIP=2
|
||||||
|
|
||||||
|
:win9xME_args_slurp
|
||||||
|
if "x%~1" == "x" goto execute
|
||||||
|
|
||||||
|
set CMD_LINE_ARGS=%*
|
||||||
|
goto execute
|
||||||
|
|
||||||
|
:4NT_args
|
||||||
|
@rem Get arguments from the 4NT Shell from JP Software
|
||||||
|
set CMD_LINE_ARGS=%$
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
|
@ -0,0 +1,48 @@
|
||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 27
|
||||||
|
buildToolsVersion '27.0.3'
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 19
|
||||||
|
targetSdkVersion 27
|
||||||
|
|
||||||
|
buildConfigField "String", "SDK_FLAVOR", '"external"'
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
jniDebuggable true
|
||||||
|
debuggable true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
main {
|
||||||
|
java.srcDirs = ['./src']
|
||||||
|
manifest.srcFile 'src/AndroidManifest.xml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.android.support:support-annotations:27.1.1'
|
||||||
|
|
||||||
|
// TODO 17974277: adal 1.13.0 and later calls its webview methods on a private thread
|
||||||
|
// msa calls its webview methods on the ui thread
|
||||||
|
// chromium expects the same thread to be used to call webview methods, will throw runtime exception otherwise
|
||||||
|
// work around by staying at adal 1.12.0 for now
|
||||||
|
|
||||||
|
// override adal 1.12.0's use of 2.2.4 to resolve dependency conflict
|
||||||
|
api 'com.google.code.gson:gson:2.7'
|
||||||
|
implementation('com.microsoft.aad:adal:1.12.0') {
|
||||||
|
exclude group: 'com.android.support'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local version of SDK, since we don't have public maven (yet)
|
||||||
|
implementation(name:'connecteddevices-sdk', ext:'aar')
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
# OneSDK Consumer Proguard Rules
|
||||||
|
# This file defines Proguard rules required by the OneSDK, and is intended to be distributed
|
||||||
|
# as part of the Android library for consuming apps.
|
||||||
|
|
||||||
|
# Set attributes to modify default Proguard behavior:
|
||||||
|
# *Annotation* - Prevents annotation removal
|
||||||
|
# Signature - Preserves generic method signature info
|
||||||
|
# SourceFile - Preservces source file info for stack traces
|
||||||
|
# LineNumberTable - Preserves original line number info for stack traces
|
||||||
|
# EnclosingMethod - Preserves info for methods in which classes are defined
|
||||||
|
# InnerClasses - Prevents removal of inner classes
|
||||||
|
-keepattributes *Annotation*, Signature, SourceFile, LineNumberTable, EnclosingMethod, InnerClasses
|
||||||
|
|
||||||
|
# Prevent removal of the Keep annotation
|
||||||
|
-keep @interface android.support.annotation.Keep
|
||||||
|
|
||||||
|
# Ensure Keep annotated classes and interfaces are actually kept
|
||||||
|
-keep @android.support.annotation.Keep class * {*;}
|
||||||
|
-keep @android.support.annotation.Keep interface * {*;}
|
||||||
|
|
||||||
|
# Preserve public inner interfaces (like the InnerClasses attribute, but no corresponding InnerInterfaces attribute exists)
|
||||||
|
-keep public interface **$* {*;}
|
||||||
|
|
||||||
|
# Preserve native method signatures
|
||||||
|
-keepclasseswithmembernames,includedescriptorclasses class * {
|
||||||
|
native <methods>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve enum methods
|
||||||
|
# Don't use 'allowoptimization' option as it prevents lookup via reflection
|
||||||
|
-keepclassmembers enum * {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Preserve inner enum class methods
|
||||||
|
# Don't use 'allowoptimization' option as it prevents lookup via reflection
|
||||||
|
-keepclassmembers enum **$* {
|
||||||
|
public static **[] values();
|
||||||
|
public static ** valueOf(java.lang.String);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore warnings for the Bond libraries from CLL since they are not used
|
||||||
|
-dontwarn com.microsoft.bond.**
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="com.microsoft.connecteddevices.sampleaccountproviders" />
|
|
@ -0,0 +1,290 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.CookieManager;
|
||||||
|
import android.webkit.CookieSyncManager;
|
||||||
|
import android.webkit.ValueCallback;
|
||||||
|
|
||||||
|
import com.microsoft.aad.adal.ADALError;
|
||||||
|
import com.microsoft.aad.adal.AuthenticationCallback;
|
||||||
|
import com.microsoft.aad.adal.AuthenticationContext;
|
||||||
|
import com.microsoft.aad.adal.AuthenticationException;
|
||||||
|
import com.microsoft.aad.adal.AuthenticationResult;
|
||||||
|
import com.microsoft.aad.adal.AuthenticationResult.AuthenticationStatus;
|
||||||
|
import com.microsoft.aad.adal.PromptBehavior;
|
||||||
|
import com.microsoft.aad.adal.TokenCacheItem;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.core.AccessTokenRequestStatus;
|
||||||
|
import com.microsoft.connecteddevices.core.AccessTokenResult;
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
import com.microsoft.connecteddevices.base.EventListener;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccountProvider;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccount;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccountType;
|
||||||
|
|
||||||
|
import java.lang.InterruptedException;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign in helper that provides an UserAccountProvider implementation for AAD using the ADAL library.
|
||||||
|
* To use this class, call signIn()/signOut(), then use the standard UserAccountProvider functions.
|
||||||
|
*
|
||||||
|
* Notes about AAD/ADAL:
|
||||||
|
* - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service.
|
||||||
|
* - Scope Individual permissions within a resource
|
||||||
|
* - Access Token A standard JSON web token for a given scope.
|
||||||
|
* This is the actual token/user ticket used to authenticate with CDP services.
|
||||||
|
* https://oauth.net/2/
|
||||||
|
* https://www.oauth.com/oauth2-servers/access-tokens/
|
||||||
|
* - Refresh token: A standard OAuth refresh token.
|
||||||
|
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
|
||||||
|
* ADAL manages this automatically.
|
||||||
|
* https://oauth.net/2/grant-types/refresh-token/
|
||||||
|
* - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource.
|
||||||
|
* Getting one requires the user consent to all the covered resources. ADAL manages this automatically.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class AADAccountProvider implements UserAccountProvider {
|
||||||
|
private static final String TAG = AADAccountProvider.class.getName();
|
||||||
|
|
||||||
|
private final String mClientId;
|
||||||
|
private final String mRedirectUri;
|
||||||
|
private final AuthenticationContext mAuthContext;
|
||||||
|
|
||||||
|
private UserAccount mAccount; // Initialized when signed in
|
||||||
|
|
||||||
|
private final Map<Long, EventListener<UserAccountProvider, Void>> mListenerMap = new ArrayMap<>();
|
||||||
|
private long mNextListenerId = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clientId id of the app's registration in the Azure portal
|
||||||
|
* @param redirectUri redirect uri the app is registered with in the Azure portal
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
public AADAccountProvider(String clientId, String redirectUri, Context context) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mRedirectUri = redirectUri;
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
CookieSyncManager.createInstance(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
mAuthContext = new AuthenticationContext(context, "https://login.microsoftonline.com/common", false);
|
||||||
|
|
||||||
|
Log.i(TAG, "Checking if previous AADAccountProvider session can be loaded...");
|
||||||
|
Iterator<TokenCacheItem> tokenCacheItems = mAuthContext.getCache().getAll();
|
||||||
|
while (tokenCacheItems.hasNext()) {
|
||||||
|
TokenCacheItem item = tokenCacheItems.next();
|
||||||
|
if (item.getIsMultiResourceRefreshToken() && item.getClientId().equals(mClientId)) {
|
||||||
|
mAccount = new UserAccount(item.getUserInfo().getUserId(), UserAccountType.AAD);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mAccount != null) {
|
||||||
|
Log.i(TAG, "Loaded previous AADAccountProvider session, starting as signed in.");
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "No previous AADAccountProvider session could be loaded, starting as signed out.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncOperation<Void> notifyListenersAsync() {
|
||||||
|
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||||
|
@Override
|
||||||
|
public Void get() {
|
||||||
|
for (EventListener<UserAccountProvider, Void> listener : mListenerMap.values()) {
|
||||||
|
listener.onEvent(AADAccountProvider.this, null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return mClientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isSignedIn() {
|
||||||
|
return mAccount != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized AsyncOperation<Boolean> signIn() throws IllegalStateException {
|
||||||
|
if (isSignedIn()) {
|
||||||
|
throw new IllegalStateException("AADAccountProvider: Already signed in!");
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsyncOperation<Boolean> ret = new AsyncOperation<>();
|
||||||
|
|
||||||
|
// If the user has not previously consented for this default resource for this app,
|
||||||
|
// the interactive flow will ask for user consent for all resources used by the app.
|
||||||
|
// If the user previously consented to this resource on this app, and more resources are added to the app later on,
|
||||||
|
// a consent prompt for all app resources will be raised when an access token for a new resource is requested -
|
||||||
|
// see getAccessTokenForUserAccountAsync()
|
||||||
|
final String defaultResource = "https://graph.windows.net";
|
||||||
|
|
||||||
|
mAuthContext.acquireToken( //
|
||||||
|
defaultResource, // resource
|
||||||
|
mClientId, // clientId
|
||||||
|
mRedirectUri, // redirectUri
|
||||||
|
null, // loginHint
|
||||||
|
PromptBehavior.Auto, // promptBehavior
|
||||||
|
null, // extraQueryParameters
|
||||||
|
new AuthenticationCallback<AuthenticationResult>() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception e) {
|
||||||
|
Log.e(TAG, "acquireToken encountered an exception: " + e.toString() + ". This may be transient.");
|
||||||
|
ret.complete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(AuthenticationResult result) {
|
||||||
|
if (result == null || result.getStatus() != AuthenticationStatus.Succeeded || result.getUserInfo() == null) {
|
||||||
|
ret.complete(false);
|
||||||
|
} else {
|
||||||
|
mAccount = new UserAccount(result.getUserInfo().getUserId(), UserAccountType.AAD);
|
||||||
|
ret.complete(true);
|
||||||
|
notifyListenersAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void signOut() throws IllegalStateException {
|
||||||
|
if (!isSignedIn()) {
|
||||||
|
throw new IllegalStateException("AADAccountProvider: Not currently signed in!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete cookies
|
||||||
|
final CookieManager cookieManager = CookieManager.getInstance();
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
cookieManager.removeAllCookie();
|
||||||
|
CookieSyncManager.getInstance().sync();
|
||||||
|
} else {
|
||||||
|
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
|
||||||
|
@Override
|
||||||
|
@TargetApi(21)
|
||||||
|
public void onReceiveValue(Boolean value) {
|
||||||
|
cookieManager.flush();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mAccount = null;
|
||||||
|
mAuthContext.getCache().removeAll();
|
||||||
|
notifyListenersAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized UserAccount[] getUserAccounts() {
|
||||||
|
if (mAccount != null) {
|
||||||
|
return new UserAccount[] { mAccount };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserAccount[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized AsyncOperation<AccessTokenResult> getAccessTokenForUserAccountAsync(
|
||||||
|
final String userAccountId, final String[] scopes) {
|
||||||
|
if (mAccount == null || !mAccount.getId().equals(userAccountId)) {
|
||||||
|
return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
final AsyncOperation<AccessTokenResult> ret = new AsyncOperation<>();
|
||||||
|
mAuthContext.acquireTokenSilentAsync(scopes[0], mClientId, mAccount.getId(), new AuthenticationCallback<AuthenticationResult>() {
|
||||||
|
@Override
|
||||||
|
public void onError(Exception e) {
|
||||||
|
if ((e instanceof AuthenticationException) &&
|
||||||
|
((AuthenticationException)e).getCode() == ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED) {
|
||||||
|
// This error only returns from acquireTokenSilentAsync when an interactive prompt is needed.
|
||||||
|
// ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource.
|
||||||
|
// Usually, users consent for all resources the app needs during the interactive flow in signIn().
|
||||||
|
// However, if the app adds new resources after the user consented previously, signIn() will not prompt.
|
||||||
|
// Escalate to the UI thread and do an interactive flow,
|
||||||
|
// which should raise a new consent prompt for all current app resources.
|
||||||
|
Log.i(TAG, "A resource was requested that the user did not previously consent to. "
|
||||||
|
+ "Attempting to raise an interactive consent prompt.");
|
||||||
|
|
||||||
|
final AuthenticationCallback<AuthenticationResult> reusedCallback = this; // reuse this callback
|
||||||
|
new Handler(Looper.getMainLooper())
|
||||||
|
.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mAuthContext.acquireToken(
|
||||||
|
scopes[0], mClientId, mRedirectUri, null, PromptBehavior.Auto, null, reusedCallback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(TAG, "getAccessTokenForUserAccountAsync hit an exception: " + e.toString() + ". This may be transient.");
|
||||||
|
ret.complete(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(AuthenticationResult result) {
|
||||||
|
if (result == null || result.getStatus() != AuthenticationStatus.Succeeded || TextUtils.isEmpty(result.getAccessToken())) {
|
||||||
|
|
||||||
|
ret.complete(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null));
|
||||||
|
} else {
|
||||||
|
ret.complete(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, result.getAccessToken()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long addUserAccountChangedListener(EventListener<UserAccountProvider, Void> listener) {
|
||||||
|
long id = mNextListenerId++;
|
||||||
|
mListenerMap.put(id, listener);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void removeUserAccountChangedListener(long id) {
|
||||||
|
mListenerMap.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onAccessTokenError(String userAccountId, String[] scopes, boolean isPermanentError) {
|
||||||
|
if (mAccount != null && mAccount.getId().equals(userAccountId)) {
|
||||||
|
if (isPermanentError) {
|
||||||
|
try {
|
||||||
|
signOut();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// Already signed out in between checking if signed in and now. No need to do anything.
|
||||||
|
Log.e(TAG, "Already signed out in onAccessTokenError. This error is most likely benign: " + e.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not a permanent error, try to refresh the tokens
|
||||||
|
try {
|
||||||
|
mAuthContext.acquireTokenSilentSync(scopes[0], mClientId, userAccountId);
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
Log.e(TAG, "Exception in ADAL when trying to refresh token: \'" + e.toString() + "\'");
|
||||||
|
} catch (InterruptedException e) { Log.e(TAG, "Interrupted while trying to refresh token: \'" + e.toString() + "\'"); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "onAccessTokenError was called, but AADAccountProvider was not signed in.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
import com.microsoft.connecteddevices.base.EventListener;
|
||||||
|
import com.microsoft.connecteddevices.core.AccessTokenResult;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccountProvider;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccount;
|
||||||
|
|
||||||
|
import java.util.Hashtable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign in helper that provides an UserAccountProvider implementation that works with both AAD and MSA accounts.
|
||||||
|
*
|
||||||
|
* To use this class, call signInMSA()/signOutMSA()/signInAAD()/signOutAAD(),
|
||||||
|
* then access the UserAccountProvider through getUserAccountProvider().
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class AADMSAAccountProvider implements UserAccountProvider {
|
||||||
|
private static final String TAG = AADMSAAccountProvider.class.getName();
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
SignedOut,
|
||||||
|
SignedInMSA,
|
||||||
|
SignedInAAD,
|
||||||
|
}
|
||||||
|
|
||||||
|
private MSAAccountProvider mMSAProvider;
|
||||||
|
private AADAccountProvider mAADProvider;
|
||||||
|
private EventListener<UserAccountProvider, Void> mListener;
|
||||||
|
|
||||||
|
private final Map<Long, EventListener<UserAccountProvider, Void>> mListenerMap = new Hashtable<>();
|
||||||
|
private long mNextListenerId = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param msaClientId id of the app's registration in the MSA portal
|
||||||
|
* @param msaScopeOverrides scope overrides for the app
|
||||||
|
* @param aadClientId id of the app's registration in the Azure portal
|
||||||
|
* @param aadRedirectUri redirect uri the app is registered with in the Azure portal
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
public AADMSAAccountProvider(
|
||||||
|
String msaClientId, final Map<String, String[]> msaScopeOverrides, String aadClientId, String aadRedirectUri, Context context) {
|
||||||
|
|
||||||
|
// Chain the inner events to the event provided by this helper
|
||||||
|
mListener = new EventListener<UserAccountProvider, Void>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(UserAccountProvider provider, Void aVoid) {
|
||||||
|
notifyListenersAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mMSAProvider = new MSAAccountProvider(msaClientId, msaScopeOverrides, context);
|
||||||
|
mAADProvider = new AADAccountProvider(aadClientId, aadRedirectUri, context);
|
||||||
|
|
||||||
|
if (mMSAProvider.isSignedIn() && mAADProvider.isSignedIn()) {
|
||||||
|
// Shouldn't ever happen, but if it does, sign AAD out
|
||||||
|
mAADProvider.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
mMSAProvider.addUserAccountChangedListener(mListener);
|
||||||
|
mAADProvider.addUserAccountChangedListener(mListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncOperation<Void> notifyListenersAsync() {
|
||||||
|
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||||
|
@Override
|
||||||
|
public Void get() {
|
||||||
|
for (EventListener<UserAccountProvider, Void> listener : mListenerMap.values()) {
|
||||||
|
listener.onEvent(AADMSAAccountProvider.this, null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized State getSignInState() {
|
||||||
|
if (mMSAProvider != null && mMSAProvider.isSignedIn()) {
|
||||||
|
return State.SignedInMSA;
|
||||||
|
}
|
||||||
|
if (mAADProvider != null && mAADProvider.isSignedIn()) {
|
||||||
|
return State.SignedInAAD;
|
||||||
|
}
|
||||||
|
return State.SignedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncOperation<Boolean> signInMSA(final Activity currentActivity) throws IllegalStateException {
|
||||||
|
if (getSignInState() != State.SignedOut) {
|
||||||
|
throw new IllegalStateException("Already signed into an account!");
|
||||||
|
}
|
||||||
|
return mMSAProvider.signIn(currentActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void signOutMSA(final Activity currentActivity) throws IllegalStateException {
|
||||||
|
if (getSignInState() != State.SignedInMSA) {
|
||||||
|
throw new IllegalStateException("Not currently signed into an MSA!");
|
||||||
|
}
|
||||||
|
mMSAProvider.signOut(currentActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AsyncOperation<Boolean> signInAAD() throws IllegalStateException {
|
||||||
|
if (getSignInState() != State.SignedOut) {
|
||||||
|
throw new IllegalStateException("Already signed into an account!");
|
||||||
|
}
|
||||||
|
return mAADProvider.signIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void signOutAAD() throws IllegalStateException {
|
||||||
|
if (getSignInState() != State.SignedInAAD) {
|
||||||
|
throw new IllegalStateException("Not currently signed into an AAD account!");
|
||||||
|
}
|
||||||
|
mAADProvider.signOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserAccountProvider getSignedInProvider() {
|
||||||
|
switch (getSignInState()) {
|
||||||
|
case SignedInMSA: return mMSAProvider;
|
||||||
|
case SignedInAAD: return mAADProvider;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized UserAccount[] getUserAccounts() {
|
||||||
|
UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider();
|
||||||
|
return (provider != null) ? provider.getUserAccounts() : new UserAccount[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized AsyncOperation<AccessTokenResult> getAccessTokenForUserAccountAsync(
|
||||||
|
final String userAccountId, final String[] scopes) {
|
||||||
|
UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider();
|
||||||
|
if (provider != null) {
|
||||||
|
return provider.getAccessTokenForUserAccountAsync(userAccountId, scopes);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncOperation<AccessTokenResult> ret = new AsyncOperation<AccessTokenResult>();
|
||||||
|
ret.completeExceptionally(new IllegalStateException("Not currently signed in!"));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long addUserAccountChangedListener(EventListener<UserAccountProvider, Void> listener) {
|
||||||
|
long id = mNextListenerId++;
|
||||||
|
mListenerMap.put(id, listener);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void removeUserAccountChangedListener(long id) {
|
||||||
|
mListenerMap.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onAccessTokenError(String userAccountId, String[] scopes, boolean isPermanentError) {
|
||||||
|
UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider();
|
||||||
|
if (provider != null) {
|
||||||
|
provider.onAccessTokenError(userAccountId, scopes, isPermanentError);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Not currently signed in!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public final class IOUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes UTF-8 output data to an output stream.
|
||||||
|
* This method is synchronous, and should only be used on small data sizes.
|
||||||
|
*
|
||||||
|
* @param stream Stream to write data to
|
||||||
|
* @param data Data to write
|
||||||
|
* @throws IOException Thrown if the output stream is unavailable, or encoding the data fails
|
||||||
|
*/
|
||||||
|
public static void writeUTF8Stream(OutputStream stream, String data) throws IOException {
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"))) {
|
||||||
|
writer.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the contents of a UTF-8 input stream.
|
||||||
|
* This method is synchronous, and should only be used on small data sizes.
|
||||||
|
*
|
||||||
|
* @param stream Input stream to read from
|
||||||
|
* @return All data received from the stream
|
||||||
|
* @throws IOException Thrown if the input stream is unavailable, or decoding the data fails
|
||||||
|
*/
|
||||||
|
public static String readUTF8Stream(InputStream stream) throws IOException {
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
stringBuilder.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,501 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.WebChromeClient;
|
||||||
|
import android.webkit.WebResourceError;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.core.AccessTokenRequestStatus;
|
||||||
|
import com.microsoft.connecteddevices.core.AccessTokenResult;
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
import com.microsoft.connecteddevices.base.EventListener;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccountProvider;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccount;
|
||||||
|
import com.microsoft.connecteddevices.core.UserAccountType;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample implementation of UserAccountProvider.
|
||||||
|
* Exposes a single MSA account, that the user logs into via WebView, to CDP.
|
||||||
|
* Follows OAuth2.0 protocol, but automatically refreshes tokens when they are close to expiring.
|
||||||
|
*
|
||||||
|
* Terms:
|
||||||
|
* - Scope: OAuth feature, limits what a token actually gives permissions to.
|
||||||
|
* https://www.oauth.com/oauth2-servers/scope/
|
||||||
|
* - Access token: A standard JSON web token for a given scope.
|
||||||
|
* This is the actual token/user ticket used to authenticate with CDP services.
|
||||||
|
* https://oauth.net/2/
|
||||||
|
* https://www.oauth.com/oauth2-servers/access-tokens/
|
||||||
|
* - Refresh token: A standard OAuth refresh token.
|
||||||
|
* Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire.
|
||||||
|
* This library caches one refresh token per user.
|
||||||
|
* As such, the refresh token must already be authorized/consented to for all CDP scopes that will be used in the app.
|
||||||
|
* https://oauth.net/2/grant-types/refresh-token/
|
||||||
|
* - Grant type: Type of OAuth authorization request to make (ie: token, password, auth code)
|
||||||
|
* https://oauth.net/2/grant-types/
|
||||||
|
* - Auth code: OAuth auth code, can be exchanged for a token.
|
||||||
|
* This library has the user sign in interactively for the auth code grant type,
|
||||||
|
* then retrieves the auth code from the return URL.
|
||||||
|
* https://oauth.net/2/grant-types/authorization-code/
|
||||||
|
* - Client ID: ID of an app's registration in the MSA portal. As of the time of writing, the portal uses GUIDs.
|
||||||
|
*
|
||||||
|
* The flow of this library is described below:
|
||||||
|
* Signing in
|
||||||
|
* 1. signIn() is called (now treated as signing in)
|
||||||
|
* 2. webview is presented to the user for sign in
|
||||||
|
* 3. Use authcode returned from user's sign in to fetch refresh token
|
||||||
|
* 4. Refresh token is cached - if the user does not sign out, but the app is restarted,
|
||||||
|
* the user will not need to enter their credentials/consent again when signIn() is called.
|
||||||
|
* 4. Now treated as signed in. Account is exposed to CDP. UserAccountChangedEvent is fired.
|
||||||
|
*
|
||||||
|
* While signed in
|
||||||
|
* CDP asks for access tokens
|
||||||
|
* 1. Check if access token is in cache
|
||||||
|
* 2. If not in cache, request a new access token using the cached refresh token.
|
||||||
|
* 3. If in cache but close to expiry, the access token is refreshed using the refresh token.
|
||||||
|
* The refreshed access token is returned.
|
||||||
|
* 4. If in cache and not close to expiry, just return it.
|
||||||
|
*
|
||||||
|
* Signing out
|
||||||
|
* 1. signOut() is called
|
||||||
|
* 2. webview is quickly popped up to go through the sign out URL
|
||||||
|
* 3. Cache is cleared.
|
||||||
|
* 4. Now treated as signed out. Account is no longer exposed to CDP. UserAccountChangedEvent is fired.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
public final class MSAAccountProvider implements UserAccountProvider, MSATokenCache.Listener {
|
||||||
|
|
||||||
|
// region Constants
|
||||||
|
private static final String TAG = MSAAccountProvider.class.getName();
|
||||||
|
|
||||||
|
// CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail.
|
||||||
|
// As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future.
|
||||||
|
private static final String[] KNOWN_SCOPES = {
|
||||||
|
"wl.offline_access", // read and update user info at any time
|
||||||
|
"ccs.ReadWrite", // device commanding scope
|
||||||
|
"wns.connect", // push notification scope
|
||||||
|
"https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // default useractivities scope
|
||||||
|
};
|
||||||
|
|
||||||
|
// OAuth URLs
|
||||||
|
private static final String REDIRECT_URL = "https://login.live.com/oauth20_desktop.srf";
|
||||||
|
private static final String AUTHORIZE_URL = "https://login.live.com/oauth20_authorize.srf";
|
||||||
|
private static final String LOGOUT_URL = "https://login.live.com/oauth20_logout.srf";
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Member Variables
|
||||||
|
private final String mClientId;
|
||||||
|
private final Map<String, String[]> mScopeOverrideMap;
|
||||||
|
private UserAccount mAccount = null;
|
||||||
|
private MSATokenCache mTokenCache;
|
||||||
|
private boolean mSignInSignOutInProgress;
|
||||||
|
|
||||||
|
private final Map<Long, EventListener<UserAccountProvider, Void>> mListenerMap = new ArrayMap<>();
|
||||||
|
private long mNextListenerId = 1L;
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Constructor
|
||||||
|
/**
|
||||||
|
* @param clientId id of the app's registration in the MSA portal
|
||||||
|
* @param scopeOverrides scope overrides for the app
|
||||||
|
* @param context
|
||||||
|
*/
|
||||||
|
public MSAAccountProvider(String clientId, final Map<String, String[]> scopeOverrides, Context context) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mScopeOverrideMap = scopeOverrides;
|
||||||
|
mTokenCache = new MSATokenCache(clientId, context);
|
||||||
|
mTokenCache.addListener(new MSATokenCache.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onTokenCachePermanentFailure() {
|
||||||
|
onAccessTokenError((mAccount != null ? mAccount.getId() : null), null, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mTokenCache.loadSavedRefreshToken()) {
|
||||||
|
Log.i(TAG, "Loaded previous session for MSAAccountProvider. Starting as signed in.");
|
||||||
|
mAccount = new UserAccount(UUID.randomUUID().toString(), UserAccountType.MSA);
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "No previous session could be loaded for MSAAccountProvider. Starting as signed out.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Private Helpers
|
||||||
|
private List<String> getAuthScopes(final String[] incoming) {
|
||||||
|
ArrayList<String> authScopes = new ArrayList<String>();
|
||||||
|
|
||||||
|
for (String scope : incoming) {
|
||||||
|
if (mScopeOverrideMap.containsKey(scope)) {
|
||||||
|
for (String replacement : mScopeOverrideMap.get(scope)) {
|
||||||
|
authScopes.add(replacement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authScopes.add(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authScopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncOperation<Void> notifyListenersAsync() {
|
||||||
|
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||||
|
@Override
|
||||||
|
public Void get() {
|
||||||
|
for (EventListener<UserAccountProvider, Void> listener : mListenerMap.values()) {
|
||||||
|
listener.onEvent(MSAAccountProvider.this, null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void addAccount() {
|
||||||
|
Log.i(TAG, "Adding an account.");
|
||||||
|
mAccount = new UserAccount(UUID.randomUUID().toString(), UserAccountType.MSA);
|
||||||
|
notifyListenersAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void removeAccount() {
|
||||||
|
if (isSignedIn()) {
|
||||||
|
Log.i(TAG, "Removing account.");
|
||||||
|
mAccount = null;
|
||||||
|
mTokenCache.clearTokens();
|
||||||
|
notifyListenersAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously requests a new access token for the provided scope(s) and caches it.
|
||||||
|
* This assumes that the sign in helper is currently signed in.
|
||||||
|
*/
|
||||||
|
private AsyncOperation<AccessTokenResult> requestNewAccessTokenAsync(final String scope) {
|
||||||
|
// Need the refresh token first, then can use it to request an access token
|
||||||
|
return mTokenCache.getRefreshTokenAsync()
|
||||||
|
.thenComposeAsync(new AsyncOperation.ResultFunction<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||||
|
@Override
|
||||||
|
public AsyncOperation<MSATokenRequest.Result> apply(String refreshToken) {
|
||||||
|
return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.REFRESH, scope, null, refreshToken);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenApplyAsync(new AsyncOperation.ResultFunction<MSATokenRequest.Result, AccessTokenResult>() {
|
||||||
|
@Override
|
||||||
|
public AccessTokenResult apply(MSATokenRequest.Result result) throws Throwable {
|
||||||
|
switch (result.getStatus()) {
|
||||||
|
case SUCCESS:
|
||||||
|
Log.i(TAG, "Successfully fetched access token.");
|
||||||
|
mTokenCache.setAccessToken(result.getAccessToken(), scope, result.getExpiresIn());
|
||||||
|
return new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, result.getAccessToken());
|
||||||
|
|
||||||
|
case TRANSIENT_FAILURE:
|
||||||
|
Log.e(TAG, "Requesting new access token failed temporarily, please try again.");
|
||||||
|
return new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null);
|
||||||
|
|
||||||
|
default: // PERMANENT_FAILURE
|
||||||
|
Log.e(TAG, "Permanent error occurred while fetching access token.");
|
||||||
|
onAccessTokenError(mAccount.getId(), new String[] { scope }, true);
|
||||||
|
throw new IOException("Permanent error occurred while fetching access token.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return mClientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Interactive Sign-in/out
|
||||||
|
public synchronized boolean isSignedIn() {
|
||||||
|
return mAccount != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pops up a webview for the user to sign in with their MSA, then uses the auth code returned to cache a refresh token for the user.
|
||||||
|
* If a refresh token was already cached from a previous session, it will be used instead, and no webview will be displayed.
|
||||||
|
*/
|
||||||
|
public synchronized AsyncOperation<Boolean> signIn(final Activity currentActivity) throws IllegalStateException {
|
||||||
|
if (isSignedIn() || mSignInSignOutInProgress) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String signInUrl = AUTHORIZE_URL + "?redirect_uri=" + REDIRECT_URL + "&response_type=code&client_id=" + mClientId +
|
||||||
|
"&scope=" + TextUtils.join("+", getAuthScopes(KNOWN_SCOPES));
|
||||||
|
final AsyncOperation<String> authCodeOperation = new AsyncOperation<>();
|
||||||
|
final AsyncOperation<Boolean> signInOperation = new AsyncOperation<>();
|
||||||
|
mSignInSignOutInProgress = true;
|
||||||
|
|
||||||
|
final Dialog dialog = new Dialog(currentActivity);
|
||||||
|
dialog.setContentView(R.layout.auth_dialog);
|
||||||
|
final WebView web = (WebView)dialog.findViewById(R.id.webv);
|
||||||
|
web.setWebChromeClient(new WebChromeClient());
|
||||||
|
web.getSettings().setJavaScriptEnabled(true);
|
||||||
|
web.getSettings().setDomStorageEnabled(true);
|
||||||
|
|
||||||
|
web.loadUrl(signInUrl);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
web.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
onSignInPageFinishedInternal(url, dialog, authCodeOperation, signInOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TargetApi(23)
|
||||||
|
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
||||||
|
super.onReceivedError(view, request, error);
|
||||||
|
onReceivedSignInErrorInternal(error.getDescription().toString(), authCodeOperation, signInOperation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
web.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
onSignInPageFinishedInternal(url, dialog, authCodeOperation, signInOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||||
|
onReceivedSignInErrorInternal(description, authCodeOperation, signInOperation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
authCodeOperation // chain after successfully fetching the authcode (does not execute if authCodeOperation completed exceptionally)
|
||||||
|
.thenComposeAsync(new AsyncOperation.ResultFunction<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||||
|
@Override
|
||||||
|
public AsyncOperation<MSATokenRequest.Result> apply(String authCode) {
|
||||||
|
return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.CODE, null, REDIRECT_URL, authCode);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenAcceptAsync(new AsyncOperation.ResultConsumer<MSATokenRequest.Result>() {
|
||||||
|
@Override
|
||||||
|
public void accept(MSATokenRequest.Result result) {
|
||||||
|
synchronized (MSAAccountProvider.this) {
|
||||||
|
mSignInSignOutInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getStatus() == MSATokenRequest.Result.Status.SUCCESS) {
|
||||||
|
if (result.getRefreshToken() == null) {
|
||||||
|
Log.e(TAG, "Unexpected: refresh token is null despite succeeding in refresh.");
|
||||||
|
signInOperation.complete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully fetched refresh token.");
|
||||||
|
mTokenCache.setRefreshToken(result.getRefreshToken());
|
||||||
|
addAccount();
|
||||||
|
signInOperation.complete(true);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to fetch refresh token using auth code.");
|
||||||
|
signInOperation.complete(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
dialog.setCancelable(true);
|
||||||
|
|
||||||
|
return signInOperation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the user out by going through the webview, then clears the cache and current state.
|
||||||
|
*/
|
||||||
|
public synchronized void signOut(final Activity currentActivity) throws IllegalStateException {
|
||||||
|
final String signOutUrl = LOGOUT_URL + "?client_id=" + mClientId + "&redirect_uri=" + REDIRECT_URL;
|
||||||
|
mSignInSignOutInProgress = true;
|
||||||
|
|
||||||
|
final Dialog dialog = new Dialog(currentActivity);
|
||||||
|
dialog.setContentView(R.layout.auth_dialog);
|
||||||
|
WebView web = (WebView)dialog.findViewById(R.id.webv);
|
||||||
|
|
||||||
|
web.loadUrl(signOutUrl);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
web.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
onSignOutPageFinishedInternal(url, dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@TargetApi(23)
|
||||||
|
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
||||||
|
super.onReceivedError(view, request, error);
|
||||||
|
onReceivedSignOutErrorInternal(error.getDescription().toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
web.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(WebView view, String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
onSignOutPageFinishedInternal(url, dialog);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
|
||||||
|
super.onReceivedError(view, errorCode, description, failingUrl);
|
||||||
|
onReceivedSignOutErrorInternal(description);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region UserAccountProvider Overrides
|
||||||
|
@Override
|
||||||
|
public synchronized UserAccount[] getUserAccounts() {
|
||||||
|
if (mAccount != null) {
|
||||||
|
return new UserAccount[] { mAccount };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserAccount[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized AsyncOperation<AccessTokenResult> getAccessTokenForUserAccountAsync(final String accountId, final String[] scopes) {
|
||||||
|
if (mAccount != null && accountId != null && accountId.equals(mAccount.getId()) && scopes.length > 0) {
|
||||||
|
|
||||||
|
final String scope = TextUtils.join(" ", getAuthScopes(scopes));
|
||||||
|
|
||||||
|
return mTokenCache.getAccessTokenAsync(scope).thenComposeAsync(
|
||||||
|
new AsyncOperation.ResultFunction<String, AsyncOperation<AccessTokenResult>>() {
|
||||||
|
@Override
|
||||||
|
public AsyncOperation<AccessTokenResult> apply(String accessToken) {
|
||||||
|
if (accessToken != null) {
|
||||||
|
// token already exists in the cache, can early return
|
||||||
|
return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, accessToken));
|
||||||
|
} else {
|
||||||
|
// token does not yet exist in the cache, need to request a new one
|
||||||
|
return requestNewAccessTokenAsync(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// No access token is available
|
||||||
|
return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized long addUserAccountChangedListener(EventListener<UserAccountProvider, Void> listener) {
|
||||||
|
long id = mNextListenerId++;
|
||||||
|
mListenerMap.put(id, listener);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void removeUserAccountChangedListener(long id) {
|
||||||
|
mListenerMap.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void onAccessTokenError(String accountId, String[] scopes, boolean isPermanentError) {
|
||||||
|
if (isPermanentError) {
|
||||||
|
removeAccount();
|
||||||
|
} else {
|
||||||
|
mTokenCache.markAllTokensExpired();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region MSATokenCacheListener Overrides
|
||||||
|
@Override
|
||||||
|
public void onTokenCachePermanentFailure() {
|
||||||
|
onAccessTokenError(null, null, true);
|
||||||
|
}
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Internal Helpers
|
||||||
|
|
||||||
|
private void onSignInPageFinishedInternal(
|
||||||
|
String url, Dialog dialog, AsyncOperation<String> authCodeOperation, AsyncOperation<Boolean> signInOperation) {
|
||||||
|
if (url.startsWith(REDIRECT_URL)) {
|
||||||
|
final Uri uri = Uri.parse(url);
|
||||||
|
final String code = uri.getQueryParameter("code");
|
||||||
|
final String error = uri.getQueryParameter("error");
|
||||||
|
|
||||||
|
dialog.dismiss();
|
||||||
|
|
||||||
|
if ((error != null) || (code == null) || (code.length() <= 0)) {
|
||||||
|
synchronized (MSAAccountProvider.this) {
|
||||||
|
mSignInSignOutInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
signInOperation.complete(false);
|
||||||
|
authCodeOperation.completeExceptionally(
|
||||||
|
new Exception((error != null) ? error : "Failed to authenticate with unknown error"));
|
||||||
|
} else {
|
||||||
|
authCodeOperation.complete(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onReceivedSignInErrorInternal(
|
||||||
|
String errorString, AsyncOperation<String> authCodeOperation, AsyncOperation<Boolean> signInOperation) {
|
||||||
|
Log.e(TAG, "Encountered web resource loading error while signing in: \'" + errorString + "\'");
|
||||||
|
synchronized (MSAAccountProvider.this) {
|
||||||
|
mSignInSignOutInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
signInOperation.complete(false);
|
||||||
|
authCodeOperation.completeExceptionally(new Exception(errorString));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSignOutPageFinishedInternal(String url, Dialog dialog) {
|
||||||
|
if (!url.contains("oauth20_desktop.srf")) {
|
||||||
|
// finishing off loading intermediate pages,
|
||||||
|
// e.g., input username/password page, consent interrupt page, wrong username/password page etc.
|
||||||
|
// no need to handle them, return early.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (MSAAccountProvider.this) {
|
||||||
|
mSignInSignOutInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uri uri = Uri.parse(url);
|
||||||
|
final String error = uri.getQueryParameter("error");
|
||||||
|
if (error != null) {
|
||||||
|
Log.e(TAG, "Signed out failed with error: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAccount();
|
||||||
|
dialog.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onReceivedSignOutErrorInternal(String errorString) {
|
||||||
|
Log.e(TAG, "Encountered web resource loading error while signing out: \'" + errorString + "\'");
|
||||||
|
synchronized (MSAAccountProvider.this) {
|
||||||
|
mSignInSignOutInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
|
@ -0,0 +1,486 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
import android.util.ArrayMap;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches MSA access and refresh tokens, automatically refreshing them as needed when fetching from the cache.
|
||||||
|
* Cached refresh tokens are persisted across sessions.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
final class MSATokenCache {
|
||||||
|
private static final String TAG = MSATokenCache.class.getName();
|
||||||
|
|
||||||
|
// Max number of times to try to refresh a token through transient failures
|
||||||
|
private static final int TOKEN_REFRESH_MAX_RETRIES = 3;
|
||||||
|
|
||||||
|
// How quickly to retry refreshing a token when encountering a transient failure
|
||||||
|
private static final long MSA_REFRESH_TOKEN_RETRY_SECONDS = 30 * 60; // 30 minutes
|
||||||
|
private static final long MSA_ACCESS_TOKEN_RETRY_SECONDS = 3 * 60; // 3 minutes
|
||||||
|
|
||||||
|
// How long it takes a refresh token to expire
|
||||||
|
private static final int MSA_REFRESH_TOKEN_EXPIRATION_SECONDS = 10 * 24 * 60 * 60; // 10 days
|
||||||
|
|
||||||
|
// How long before expiry to consider a token in need of a refresh.
|
||||||
|
// (MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS is intended to be aggressive and keep the refresh token relatively far from expiry)
|
||||||
|
private static final int MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS = 7 * 24 * 60 * 60; // 7 days
|
||||||
|
private static final int MSA_ACCESS_TOKEN_CLOSE_TO_EXPIRY_SECONDS = 5 * 60; // 5 minutes
|
||||||
|
|
||||||
|
private static final String MSA_OFFLINE_ACCESS_SCOPE = "wl.offline_access";
|
||||||
|
|
||||||
|
private static final ScheduledExecutorService sRetryExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function. Returns a Date n seconds from now.
|
||||||
|
*/
|
||||||
|
private static final Date getDateSecondsAfterNow(int seconds) {
|
||||||
|
return getDateSecondsAfter(null, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function. Returns a Date n seconds after date.
|
||||||
|
*/
|
||||||
|
private static final Date getDateSecondsAfter(Date date, int seconds) {
|
||||||
|
Calendar calendar = Calendar.getInstance(); // sets time to current
|
||||||
|
if (date != null) {
|
||||||
|
calendar.setTime(date);
|
||||||
|
}
|
||||||
|
calendar.add(Calendar.SECOND, seconds);
|
||||||
|
return calendar.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper class wrapping a cached access token. Responsible for refreshing it on-demand.
|
||||||
|
*/
|
||||||
|
private class MSATokenCacheItem {
|
||||||
|
protected String mToken;
|
||||||
|
protected Date mCloseToExpirationDate; // Actual expiration date is used less than this, so just cache this instead
|
||||||
|
protected final MSATokenRequest mRefreshRequest;
|
||||||
|
|
||||||
|
public MSATokenCacheItem(String token, int expiresInSeconds, MSATokenRequest refreshRequest) {
|
||||||
|
mToken = token;
|
||||||
|
mCloseToExpirationDate = getDateSecondsAfterNow(expiresInSeconds - getCloseToExpirySeconds());
|
||||||
|
mRefreshRequest = refreshRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of seconds before expiry that this token is considered in need of a refresh.
|
||||||
|
*/
|
||||||
|
protected int getCloseToExpirySeconds() {
|
||||||
|
return MSA_ACCESS_TOKEN_CLOSE_TO_EXPIRY_SECONDS; // Base class expects access tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of seconds to wait before retrying, when a refresh fails with a transient error.
|
||||||
|
*/
|
||||||
|
protected long getRetrySeconds() {
|
||||||
|
return MSA_ACCESS_TOKEN_RETRY_SECONDS; // Base class expects access tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the refresh token to use to refresh the token held by this item.
|
||||||
|
* For access tokens, this gets the refresh token held by the cache.
|
||||||
|
* For refresh tokens, this just returns the currently-held token.
|
||||||
|
*/
|
||||||
|
protected AsyncOperation<String> getRefreshTokenAsync() {
|
||||||
|
return MSATokenCache.this.getRefreshTokenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Steps to complete after a successful refresh.
|
||||||
|
* For access tokens, sets the new token and new expiration.
|
||||||
|
* For refresh tokens, marks current access tokens as expired, and caches the refresh token in persistent storage.
|
||||||
|
*/
|
||||||
|
protected synchronized void onSuccessfulRefresh(MSATokenRequest.Result result) {
|
||||||
|
Log.i(TAG, "Successfully refreshed access token.");
|
||||||
|
mToken = result.getAccessToken();
|
||||||
|
mCloseToExpirationDate = getDateSecondsAfterNow(result.getExpiresIn() - getCloseToExpirySeconds());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper - asynchronously fetches the token held by this item.
|
||||||
|
* If the token is close to expiry, refreshes it first.
|
||||||
|
* If this refresh fails due to transient error, recursively retries up to remainingRetries times to refresh.
|
||||||
|
*
|
||||||
|
* @param operation AsyncOperation to return the token on
|
||||||
|
* @param remainingRetries number of times to retry refreshing, in the case of transient error
|
||||||
|
* @return the operation that was passed in
|
||||||
|
*/
|
||||||
|
private AsyncOperation<String> _getTokenAsyncInternal(final AsyncOperation<String> operation, final int remainingRetries) {
|
||||||
|
if (!needsRefresh()) {
|
||||||
|
operation.complete(mToken); // Already have a non-stale token, can just return with it
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRefreshTokenAsync()
|
||||||
|
.thenComposeAsync(new AsyncOperation.ResultFunction<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||||
|
@Override
|
||||||
|
public AsyncOperation<MSATokenRequest.Result> apply(String refreshToken) {
|
||||||
|
return mRefreshRequest.requestAsync(refreshToken);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.thenAcceptAsync(new AsyncOperation.ResultConsumer<MSATokenRequest.Result>() {
|
||||||
|
@Override
|
||||||
|
public void accept(MSATokenRequest.Result result) {
|
||||||
|
switch (result.getStatus()) {
|
||||||
|
case SUCCESS:
|
||||||
|
onSuccessfulRefresh(result);
|
||||||
|
operation.complete(mToken);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TRANSIENT_FAILURE:
|
||||||
|
// Recursively retry the refresh, if there are still remaining retries
|
||||||
|
if (remainingRetries <= 0) {
|
||||||
|
Log.e(TAG, "Reached max number of retries for refreshing token.");
|
||||||
|
operation.complete(null);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "Transient error while refreshing token, retrying in " + getRetrySeconds() + "seconds...");
|
||||||
|
sRetryExecutor.schedule(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
_getTokenAsyncInternal(operation, remainingRetries - 1);
|
||||||
|
}
|
||||||
|
}, getRetrySeconds(), TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // PERMANENT_FAILURE
|
||||||
|
Log.e(TAG, "Permanent error occurred while refreshing token.");
|
||||||
|
MSATokenCache.this.onPermanentFailure();
|
||||||
|
operation.complete(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously fetches the token held by this item, refreshing it if necessary.
|
||||||
|
*/
|
||||||
|
public AsyncOperation<String> getTokenAsync() {
|
||||||
|
AsyncOperation<String> ret = new AsyncOperation<String>();
|
||||||
|
return _getTokenAsyncInternal(ret, TOKEN_REFRESH_MAX_RETRIES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsRefresh() {
|
||||||
|
return mCloseToExpirationDate.before(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
return getDateSecondsAfter(mCloseToExpirationDate, getCloseToExpirySeconds()).before(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void markExpired() {
|
||||||
|
mCloseToExpirationDate = new Date(0); // Start of epoch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private helper class wrapping a cached refresh token. Responsible for refreshing it on demand. Can translate to/from json format.
|
||||||
|
*/
|
||||||
|
private final class MSARefreshTokenCacheItem extends MSATokenCacheItem {
|
||||||
|
private static final String JSON_TOKEN_KEY = "refresh_token";
|
||||||
|
private static final String JSON_EXPIRATION_KEY = "expires";
|
||||||
|
|
||||||
|
public MSARefreshTokenCacheItem(String token, int expiresInSeconds, MSATokenRequest refreshRequest) {
|
||||||
|
super(token, expiresInSeconds, refreshRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MSARefreshTokenCacheItem(JSONObject json) throws IOException, JSONException, ParseException {
|
||||||
|
super(null, 0, new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, MSA_OFFLINE_ACCESS_SCOPE, null));
|
||||||
|
|
||||||
|
mToken = json.optString(JSON_TOKEN_KEY);
|
||||||
|
String dateString = json.optString(JSON_EXPIRATION_KEY);
|
||||||
|
if (mToken == null || dateString == null) {
|
||||||
|
throw new IOException("Saved refresh token was improperly formatted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Date expirationDate = DateFormat.getDateTimeInstance().parse(dateString);
|
||||||
|
mCloseToExpirationDate = getDateSecondsAfter(expirationDate, -MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getCloseToExpirySeconds() {
|
||||||
|
return MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected long getRetrySeconds() {
|
||||||
|
return MSA_REFRESH_TOKEN_RETRY_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AsyncOperation<String> getRefreshTokenAsync() {
|
||||||
|
return AsyncOperation.completedFuture(mToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized void onSuccessfulRefresh(MSATokenRequest.Result result) {
|
||||||
|
Log.i(TAG, "Successfully refreshed refresh token.");
|
||||||
|
mToken = result.getRefreshToken();
|
||||||
|
mCloseToExpirationDate =
|
||||||
|
getDateSecondsAfterNow(MSA_REFRESH_TOKEN_EXPIRATION_SECONDS - MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS);
|
||||||
|
MSATokenCache.this.markAccessTokensExpired();
|
||||||
|
MSATokenCache.this.trySaveRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized JSONObject toJSON() throws JSONException {
|
||||||
|
// Get the actual expiration date
|
||||||
|
Date expirationDate = getDateSecondsAfter(mCloseToExpirationDate, MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS);
|
||||||
|
|
||||||
|
JSONObject ret = new JSONObject();
|
||||||
|
ret.put(JSON_TOKEN_KEY, mToken);
|
||||||
|
ret.put(JSON_EXPIRATION_KEY, DateFormat.getDateTimeInstance().format(expirationDate));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides callbacks when the cache encounters a permanent failure and has to wipe its state.
|
||||||
|
*/
|
||||||
|
public static interface Listener { void onTokenCachePermanentFailure(); }
|
||||||
|
|
||||||
|
private final String mClientId;
|
||||||
|
private final Context mContext;
|
||||||
|
|
||||||
|
private MSARefreshTokenCacheItem mCachedRefreshToken = null;
|
||||||
|
private final Map<String, MSATokenCacheItem> mCachedAccessTokens = new ArrayMap<>();
|
||||||
|
|
||||||
|
private final Collection<Listener> mListeners = new ArrayList<>();
|
||||||
|
|
||||||
|
public MSATokenCache(String clientId, Context context) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a file in application-specific storage that's used to persist the refresh token across sessions.
|
||||||
|
*/
|
||||||
|
private File getRefreshTokenSaveFile() throws IOException {
|
||||||
|
Context appContext = mContext.getApplicationContext();
|
||||||
|
File appDirectory = appContext.getDir(appContext.getPackageName(), Context.MODE_PRIVATE);
|
||||||
|
if (appDirectory == null) {
|
||||||
|
throw new IOException("Could not access app directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File(appDirectory, "samplemsaaccountprovider.dat");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to save the current refresh token to persistent storage.
|
||||||
|
*/
|
||||||
|
private void trySaveRefreshToken() {
|
||||||
|
Log.i(TAG, "Trying to save refresh token...");
|
||||||
|
try {
|
||||||
|
File file = getRefreshTokenSaveFile();
|
||||||
|
JSONObject json = file.exists() ? new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file))) : new JSONObject();
|
||||||
|
|
||||||
|
json.put(mClientId, mCachedRefreshToken.toJSON());
|
||||||
|
IOUtil.writeUTF8Stream(new FileOutputStream(file), json.toString());
|
||||||
|
|
||||||
|
Log.i(TAG, "Saved refresh token.");
|
||||||
|
|
||||||
|
} catch (IOException | JSONException e) {
|
||||||
|
Log.e(TAG, "Exception while saving refresh token. \"" + e.getLocalizedMessage() + "\" Will not save.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to read a saved refresh token from persistent storage, and return it as an MSARefreshTokenItem.
|
||||||
|
*/
|
||||||
|
private MSARefreshTokenCacheItem tryReadSavedRefreshToken() {
|
||||||
|
Log.i(TAG, "Trying to read saved refresh token...");
|
||||||
|
try {
|
||||||
|
File file = getRefreshTokenSaveFile();
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
Log.i(TAG, "No saved refresh token was found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject json = new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file)));
|
||||||
|
JSONObject innerJson = json.optJSONObject(mClientId);
|
||||||
|
|
||||||
|
if (innerJson == null) {
|
||||||
|
Log.i(TAG, "Could not read saved refresh token.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Read saved refresh token.");
|
||||||
|
return new MSARefreshTokenCacheItem(innerJson);
|
||||||
|
|
||||||
|
} catch (IOException | JSONException | ParseException e) {
|
||||||
|
Log.e(TAG, "Exception reading saved refresh token. \"" + e.getLocalizedMessage() + "\"");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to delete the saved refresh token for this app in persistent storage.
|
||||||
|
*/
|
||||||
|
private void tryClearSavedRefreshToken() {
|
||||||
|
Log.i(TAG, "Trying to delete saved refresh token...");
|
||||||
|
try {
|
||||||
|
File file = getRefreshTokenSaveFile();
|
||||||
|
if (!file.exists()) {
|
||||||
|
Log.i(TAG, "No saved refresh token was found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try to remove just a section of the json corresponding to client id
|
||||||
|
JSONObject json = new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file)));
|
||||||
|
json.remove(mClientId);
|
||||||
|
|
||||||
|
if (json.length() <= 0) {
|
||||||
|
file.delete(); // Just delete the file if the json would be empty
|
||||||
|
} else {
|
||||||
|
IOUtil.writeUTF8Stream(new FileOutputStream(file), json.toString());
|
||||||
|
}
|
||||||
|
} catch (JSONException e) {
|
||||||
|
// Failed to parse the json, just delete everything
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Deleted saved refresh token.");
|
||||||
|
|
||||||
|
} catch (IOException e) { Log.e(TAG, "Failed to delete saved refresh token. \"" + e.getLocalizedMessage() + "\""); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks access tokens as expired, such that a refresh is performed before returning, when the access token is next requested.
|
||||||
|
*/
|
||||||
|
private synchronized void markAccessTokensExpired() {
|
||||||
|
for (MSATokenCacheItem cachedAccessToken : mCachedAccessTokens.values()) {
|
||||||
|
cachedAccessToken.markExpired();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls back any listeners that the cache has encountered a permanent failure, and that they should perform any needed error-handling.
|
||||||
|
*/
|
||||||
|
private void onPermanentFailure() {
|
||||||
|
clearTokens();
|
||||||
|
for (Listener listener : mListeners) {
|
||||||
|
listener.onTokenCachePermanentFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRefreshToken(String refreshToken) {
|
||||||
|
MSATokenRequest refreshRequest = new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, MSA_OFFLINE_ACCESS_SCOPE, null);
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
mCachedRefreshToken = new MSARefreshTokenCacheItem(refreshToken, MSA_REFRESH_TOKEN_EXPIRATION_SECONDS, refreshRequest);
|
||||||
|
markAccessTokensExpired();
|
||||||
|
trySaveRefreshToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessToken(String accessToken, String scope, int expiresInSeconds) {
|
||||||
|
MSATokenRequest refreshRequest = new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, scope, null);
|
||||||
|
|
||||||
|
synchronized (this) {
|
||||||
|
mCachedAccessTokens.put(scope, new MSATokenCacheItem(accessToken, expiresInSeconds, refreshRequest));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized AsyncOperation<String> getRefreshTokenAsync() {
|
||||||
|
if (mCachedRefreshToken != null) {
|
||||||
|
return mCachedRefreshToken.getTokenAsync();
|
||||||
|
} else {
|
||||||
|
return AsyncOperation.completedFuture(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized AsyncOperation<String> getAccessTokenAsync(String scope) {
|
||||||
|
MSATokenCacheItem cachedAccessToken = mCachedAccessTokens.get(scope);
|
||||||
|
if (cachedAccessToken != null) {
|
||||||
|
return cachedAccessToken.getTokenAsync();
|
||||||
|
} else {
|
||||||
|
return AsyncOperation.completedFuture(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void addListener(Listener listener) {
|
||||||
|
mListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void removeListener(Listener listener) {
|
||||||
|
mListeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> allScopes() {
|
||||||
|
return mCachedAccessTokens.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to load a saved refresh token from disk. If successful, the loaded refresh token is used as this cache's refresh token.
|
||||||
|
* @return Whether a saved refresh token was loaded successfully.
|
||||||
|
*/
|
||||||
|
public boolean loadSavedRefreshToken() {
|
||||||
|
Log.i(TAG, "Trying to load saved refresh token...");
|
||||||
|
MSARefreshTokenCacheItem savedRefreshToken = tryReadSavedRefreshToken();
|
||||||
|
|
||||||
|
if (savedRefreshToken == null) {
|
||||||
|
Log.i(TAG, "Failed to load saved refresh token.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedRefreshToken.isExpired()) {
|
||||||
|
Log.i(TAG, "Read saved refresh token, but was expired. Ignoring.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Successfully loaded saved refresh token.");
|
||||||
|
mCachedRefreshToken = savedRefreshToken;
|
||||||
|
markAllTokensExpired(); // Force a refresh on everything on first use
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all tokens from the cache, and any saved refresh tokens belonging to this app in persistent storage.
|
||||||
|
*/
|
||||||
|
public synchronized void clearTokens() {
|
||||||
|
mCachedAccessTokens.clear();
|
||||||
|
mCachedRefreshToken = null;
|
||||||
|
tryClearSavedRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks all tokens as expired, such that a refresh is performed before returning, when a token is next requested.
|
||||||
|
*/
|
||||||
|
public synchronized void markAllTokensExpired() {
|
||||||
|
mCachedRefreshToken.markExpired();
|
||||||
|
markAccessTokensExpired();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
package com.microsoft.connecteddevices.sampleaccountproviders;
|
||||||
|
|
||||||
|
import android.support.annotation.Keep;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates a noninteractive request for an MSA token.
|
||||||
|
* This request may be performed multiple times.
|
||||||
|
*/
|
||||||
|
@Keep
|
||||||
|
final class MSATokenRequest {
|
||||||
|
|
||||||
|
private static final String TAG = MSATokenRequest.class.getName();
|
||||||
|
|
||||||
|
// OAuth Token Grant Type
|
||||||
|
public static final class GrantType {
|
||||||
|
public static final String CODE = "authorization_code";
|
||||||
|
public static final String REFRESH = "refresh_token";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class encapsulating the result of an MSATokenRequest.
|
||||||
|
*/
|
||||||
|
public static final class Result {
|
||||||
|
public static enum Status { SUCCESS, TRANSIENT_FAILURE, PERMANENT_FAILURE }
|
||||||
|
|
||||||
|
private final Status mStatus;
|
||||||
|
private String mAccessToken = null;
|
||||||
|
private String mRefreshToken = null;
|
||||||
|
private int mExpiresIn = 0;
|
||||||
|
|
||||||
|
Result(Status status, JSONObject responseJson) {
|
||||||
|
mStatus = status;
|
||||||
|
|
||||||
|
if (responseJson != null) {
|
||||||
|
mAccessToken = responseJson.optString("access_token", null);
|
||||||
|
mRefreshToken = responseJson.optString("refresh_token", null);
|
||||||
|
mExpiresIn = responseJson.optInt("expires_in"); // returns 0 if this key doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Status getStatus() {
|
||||||
|
return mStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessToken() {
|
||||||
|
return mAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRefreshToken() {
|
||||||
|
return mRefreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getExpiresIn() {
|
||||||
|
return mExpiresIn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String mClientId;
|
||||||
|
private final String mGrantType;
|
||||||
|
private final String mScope;
|
||||||
|
private final String mRedirectUri;
|
||||||
|
|
||||||
|
public MSATokenRequest(String clientId, String grantType, String scope, String redirectUri) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mGrantType = grantType;
|
||||||
|
mScope = scope;
|
||||||
|
mRedirectUri = redirectUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a query string from a list of name-value pairs.
|
||||||
|
*
|
||||||
|
* @param params Name-value pairs to compose the query string from
|
||||||
|
* @return A query string composed of the provided name-value pairs
|
||||||
|
* @throws UnsupportedEncodingException Thrown if encoding a name or value fails
|
||||||
|
*/
|
||||||
|
private static String getQueryString(List<Pair<String, String>> params) throws UnsupportedEncodingException {
|
||||||
|
StringBuilder queryStringBuilder = new StringBuilder();
|
||||||
|
boolean isFirstParam = true;
|
||||||
|
for (Pair<String, String> param : params) {
|
||||||
|
if (isFirstParam) {
|
||||||
|
isFirstParam = false;
|
||||||
|
} else {
|
||||||
|
queryStringBuilder.append("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
queryStringBuilder.append(URLEncoder.encode(param.first, "UTF-8"));
|
||||||
|
queryStringBuilder.append("=");
|
||||||
|
queryStringBuilder.append(URLEncoder.encode(param.second, "UTF-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryStringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch Token (Access or Refresh Token).
|
||||||
|
* @param clientId - clientId of the app's registration in the MSA portal
|
||||||
|
* @param grantType - one of the MSATokenRequest.GrantType constants
|
||||||
|
* @param scope
|
||||||
|
* @param redirectUri
|
||||||
|
* @param token - authCode for GrantType.CODE, or refresh token for GrantType.REFRESH
|
||||||
|
*/
|
||||||
|
public static AsyncOperation<MSATokenRequest.Result> requestAsync(
|
||||||
|
final String clientId, final String grantType, final String scope, final String redirectUri, final String token) {
|
||||||
|
if (token == null || token.length() <= 0) {
|
||||||
|
Log.e(TAG, "Refresh token or auth code for MSATokenRequest was unexpectedly empty - treating as permanent failure.");
|
||||||
|
return AsyncOperation.completedFuture(new MSATokenRequest.Result(Result.Status.PERMANENT_FAILURE, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<MSATokenRequest.Result>() {
|
||||||
|
@Override
|
||||||
|
public MSATokenRequest.Result get() {
|
||||||
|
HttpsURLConnection connection = null;
|
||||||
|
MSATokenRequest.Result.Status status = Result.Status.TRANSIENT_FAILURE;
|
||||||
|
JSONObject responseJson = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Build the query string
|
||||||
|
List<Pair<String, String>> params = new LinkedList<>();
|
||||||
|
params.add(new Pair<>("client_id", clientId));
|
||||||
|
params.add(new Pair<>("grant_type", grantType));
|
||||||
|
|
||||||
|
if (grantType.equals(GrantType.CODE)) {
|
||||||
|
params.add(new Pair<>("redirect_uri", redirectUri));
|
||||||
|
params.add(new Pair<>("code", token));
|
||||||
|
} else if (grantType.equals(GrantType.REFRESH)) {
|
||||||
|
params.add(new Pair<>("scope", scope));
|
||||||
|
params.add(new Pair<>(grantType, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
String queryString = getQueryString(params);
|
||||||
|
|
||||||
|
// Write the query string
|
||||||
|
URL url = new URL("https://login.live.com/oauth20_token.srf");
|
||||||
|
connection = (HttpsURLConnection)url.openConnection();
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
IOUtil.writeUTF8Stream(connection.getOutputStream(), queryString);
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode >= 500) {
|
||||||
|
status = Result.Status.TRANSIENT_FAILURE;
|
||||||
|
} else if (responseCode >= 400) {
|
||||||
|
status = Result.Status.PERMANENT_FAILURE;
|
||||||
|
} else if ((responseCode >= 200 && responseCode < 300) || responseCode == 304) {
|
||||||
|
status = Result.Status.SUCCESS;
|
||||||
|
} else {
|
||||||
|
status = Result.Status.TRANSIENT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == Result.Status.SUCCESS) {
|
||||||
|
responseJson = new JSONObject(IOUtil.readUTF8Stream(connection.getInputStream()));
|
||||||
|
} else {
|
||||||
|
Map<String, List<String>> heads = connection.getHeaderFields();
|
||||||
|
String error = IOUtil.readUTF8Stream(connection.getErrorStream());
|
||||||
|
Log.e(TAG, "Failed to get token with HTTP code: " + responseCode + heads + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException | JSONException e) {
|
||||||
|
Log.e(TAG, "Failed to get token: \"" + e.getLocalizedMessage() + "\"");
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
return new MSATokenRequest.Result(status, responseJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch token (Access or Refresh Token).
|
||||||
|
* @param token - authCode for GrantType.CODE, or refresh token for GrantType.REFRESH
|
||||||
|
*/
|
||||||
|
public AsyncOperation<MSATokenRequest.Result> requestAsync(String token) {
|
||||||
|
return requestAsync(mClientId, mGrantType, mScope, mRedirectUri, token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?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" >
|
||||||
|
<WebView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/webv"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,4 @@
|
||||||
|
include ':app',
|
||||||
|
':sampleaccountproviders'
|
||||||
|
|
||||||
|
project(':sampleaccountproviders').projectDir = new File('sampleaccountproviders/android')
|
|
@ -0,0 +1,44 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<Page
|
||||||
|
x:Class="SDKTemplate.AccountsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="12,20,12,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||||
|
<TextBlock Name="Description" Style="{StaticResource ScenarioDescriptionTextStyle}" TextWrapping="Wrap">
|
||||||
|
Please login with MSA or AAD account
|
||||||
|
</TextBlock>
|
||||||
|
<Button x:Name="AadButton" Content="Login with AAD" Margin="0,10,0,0" Click="Button_LoginAAD"/>
|
||||||
|
<Button x:Name="MsaButton" Content="Login with MSA" Margin="0,10,0,0" Click="Button_LoginMSA"/>
|
||||||
|
<Button x:Name="LogoutButton" Content="Logout" Margin="0,10,0,0" Click="Button_Logout"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Status Block for providing messages to the user. Use the
|
||||||
|
NotifyUser() method to populate the message -->
|
||||||
|
<TextBlock x:Name="StatusBlock" Grid.Row="2" Margin="12, 10, 12, 10" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
|
@ -0,0 +1,105 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Windows.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public sealed partial class AccountsPage : Page
|
||||||
|
{
|
||||||
|
private MainPage rootPage;
|
||||||
|
private MicrosoftAccountProvider accountProvider;
|
||||||
|
|
||||||
|
public AccountsPage()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
rootPage = MainPage.Current;
|
||||||
|
accountProvider = ((App)Application.Current).AccountProvider;
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUI()
|
||||||
|
{
|
||||||
|
MsaButton.IsEnabled = (accountProvider.SignedInAccount == null);
|
||||||
|
AadButton.IsEnabled = (accountProvider.SignedInAccount == null);
|
||||||
|
LogoutButton.IsEnabled = (accountProvider.SignedInAccount != null);
|
||||||
|
LogoutButton.Content = "Logout";
|
||||||
|
|
||||||
|
if (accountProvider.SignedInAccount != null)
|
||||||
|
{
|
||||||
|
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
|
||||||
|
if (accountProvider.AadUser != null)
|
||||||
|
{
|
||||||
|
Description.Text += accountProvider.AadUser.DisplayableId;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogoutButton.Content = $"Logout - {accountProvider.SignedInAccount.Type}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Button_LoginMSA(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (accountProvider.SignedInAccount == null)
|
||||||
|
{
|
||||||
|
((Button)sender).IsEnabled = false;
|
||||||
|
|
||||||
|
bool success = await accountProvider.SignInMsa();
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("MSA login failed!", NotifyType.ErrorMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("MSA login successful", NotifyType.StatusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Button_LoginAAD(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (accountProvider.SignedInAccount == null)
|
||||||
|
{
|
||||||
|
((Button)sender).IsEnabled = false;
|
||||||
|
|
||||||
|
bool success = await accountProvider.SignInAad();
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("AAD login failed!", NotifyType.ErrorMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("AAD login successful", NotifyType.StatusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Button_Logout(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
((Button)sender).IsEnabled = false;
|
||||||
|
|
||||||
|
accountProvider.SignOut();
|
||||||
|
|
||||||
|
rootPage.NotifyUser("Logout successful", NotifyType.ErrorMessage);
|
||||||
|
|
||||||
|
UpdateUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<Application
|
||||||
|
x:Class="SDKTemplate.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate"
|
||||||
|
RequestedTheme="Dark">
|
||||||
|
|
||||||
|
<Application.Resources>
|
||||||
|
<!-- Application-specific resources -->
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Styles that define common aspects of the platform look and feel
|
||||||
|
Required by Visual Studio project and item templates
|
||||||
|
-->
|
||||||
|
<ResourceDictionary Source="/Styles/Styles.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
|
||||||
|
</Application>
|
|
@ -0,0 +1,177 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.ApplicationModel.Activation;
|
||||||
|
using Windows.ApplicationModel.Background;
|
||||||
|
using Windows.Networking.PushNotifications;
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Windows.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=402347&clcid=0x409
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public class AppLauchArgs
|
||||||
|
{
|
||||||
|
public string type { get; set; }
|
||||||
|
public string notificationId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides application-specific behavior to supplement the default Application class.
|
||||||
|
/// </summary>
|
||||||
|
sealed partial class App : Application
|
||||||
|
{
|
||||||
|
public PushNotificationChannel PushChannel { get; set; }
|
||||||
|
public MicrosoftAccountProvider AccountProvider { get; set; }
|
||||||
|
public GraphNotificationProvider NotificationProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the singleton application object. This is the first line of authored code
|
||||||
|
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||||
|
/// </summary>
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the application is launched normally by the end user. Other entry points
|
||||||
|
/// will be used such as when the application is launched to open a specific file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="e">Details about the launch request and process.</param>
|
||||||
|
protected override async void OnLaunched(LaunchActivatedEventArgs e)
|
||||||
|
{
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
if (System.Diagnostics.Debugger.IsAttached)
|
||||||
|
{
|
||||||
|
this.DebugSettings.EnableFrameRateCounter = false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Logger.Instance.LogMessage($"App Launched with {e.Arguments}");
|
||||||
|
|
||||||
|
if (PushChannel == null)
|
||||||
|
{
|
||||||
|
PushChannel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
|
||||||
|
Logger.Instance.LogMessage($"Setup Push {PushChannel.Uri}");
|
||||||
|
PushChannel.PushNotificationReceived += PushNotificationReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NotificationProvider == null)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Setup AccountsProvider and NotificationsProvider");
|
||||||
|
AccountProvider = new MicrosoftAccountProvider();
|
||||||
|
NotificationProvider = new GraphNotificationProvider(AccountProvider, PushChannel.Uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
Frame rootFrame = Window.Current.Content as Frame;
|
||||||
|
|
||||||
|
// Do not repeat app initialization when the Window already has content,
|
||||||
|
// just ensure that the window is active
|
||||||
|
if (rootFrame == null)
|
||||||
|
{
|
||||||
|
// Create a Frame to act as the navigation context and navigate to the first page
|
||||||
|
rootFrame = new Frame
|
||||||
|
{
|
||||||
|
// Set the default language
|
||||||
|
Language = Windows.Globalization.ApplicationLanguages.Languages[0]
|
||||||
|
};
|
||||||
|
|
||||||
|
rootFrame.NavigationFailed += OnNavigationFailed;
|
||||||
|
|
||||||
|
if (e.PreviousExecutionState == ApplicationExecutionState.Terminated)
|
||||||
|
{
|
||||||
|
// Load state from previously suspended application
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the frame in the current Window
|
||||||
|
Window.Current.Content = rootFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootFrame.Content == null)
|
||||||
|
{
|
||||||
|
// When the navigation stack isn't restored navigate to the first page,
|
||||||
|
// configuring the new page by passing required information as a navigation
|
||||||
|
// parameter
|
||||||
|
rootFrame.Navigate(typeof(MainPage), e.Arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(e.Arguments))
|
||||||
|
{
|
||||||
|
var result = JsonConvert.DeserializeObject<AppLauchArgs>(e.Arguments);
|
||||||
|
NotificationProvider?.Refresh();
|
||||||
|
NotificationProvider?.Activate(result.notificationId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the current window is active
|
||||||
|
Window.Current.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when Navigation to a certain page fails
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender">The Frame which failed navigation</param>
|
||||||
|
/// <param name="e">Details about the navigation failure</param>
|
||||||
|
void OnNavigationFailed(object sender, NavigationFailedEventArgs e)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Push received type:{e.NotificationType}");
|
||||||
|
if (e.NotificationType == PushNotificationType.Raw)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
NotificationProvider?.ReceiveNotification(e.RawNotification.Content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BackgroundTaskDeferral m_deferral;
|
||||||
|
protected override async void OnBackgroundActivated(BackgroundActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
base.OnBackgroundActivated(args);
|
||||||
|
m_deferral = args.TaskInstance.GetDeferral();
|
||||||
|
args.TaskInstance.Canceled += (s, r) =>
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Task canceled for {r}");
|
||||||
|
m_deferral.Complete();
|
||||||
|
};
|
||||||
|
Logger.Instance.LogMessage($"{args.TaskInstance.Task.Name} activated in background");
|
||||||
|
|
||||||
|
if (args.TaskInstance.TriggerDetails is RawNotification)
|
||||||
|
{
|
||||||
|
var notification = args.TaskInstance.TriggerDetails as RawNotification;
|
||||||
|
Logger.Instance.LogMessage($"RawNotification received {notification.Content}");
|
||||||
|
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
if (NotificationProvider != null)
|
||||||
|
{
|
||||||
|
AccountProvider = new MicrosoftAccountProvider();
|
||||||
|
NotificationProvider = new GraphNotificationProvider(AccountProvider, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationProvider.ReceiveNotification(notification.Content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(30));
|
||||||
|
Logger.Instance.LogMessage($"Task completed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
После Ширина: | Высота: | Размер: 3.3 KiB |
После Ширина: | Высота: | Размер: 8.8 KiB |
После Ширина: | Высота: | Размер: 4.5 KiB |
После Ширина: | Высота: | Размер: 670 B |
После Ширина: | Высота: | Размер: 9.1 KiB |
После Ширина: | Высота: | Размер: 1.1 KiB |
После Ширина: | Высота: | Размер: 442 B |
После Ширина: | Высота: | Размер: 4.7 KiB |
После Ширина: | Высота: | Размер: 2.9 KiB |
|
@ -0,0 +1,280 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using Microsoft.ConnectedDevices.Core;
|
||||||
|
using Microsoft.ConnectedDevices.UserData;
|
||||||
|
using Microsoft.ConnectedDevices.UserNotifications;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.Data.Xml.Dom;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Storage;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public class GraphNotificationProvider : IConnectedDevicesNotificationProvider
|
||||||
|
{
|
||||||
|
private ConnectedDevicesPlatform m_platform;
|
||||||
|
private UserDataFeed m_feed;
|
||||||
|
private UserNotificationReader m_reader;
|
||||||
|
private UserNotificationChannel m_channel;
|
||||||
|
private List<UserNotification> m_newNotifications = new List<UserNotification>();
|
||||||
|
private List<UserNotification> m_historicalNotifications = new List<UserNotification>();
|
||||||
|
private MicrosoftAccountProvider m_accoutProvider;
|
||||||
|
private string m_pushUri;
|
||||||
|
|
||||||
|
static readonly string PushUriKey = "PushUri";
|
||||||
|
|
||||||
|
public event EventHandler CacheUpdated;
|
||||||
|
|
||||||
|
public bool NewNotifications
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return m_newNotifications.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<UserNotification> HistoricalNotifications
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return m_historicalNotifications.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public GraphNotificationProvider(MicrosoftAccountProvider accountProvider, string pushUri)
|
||||||
|
{
|
||||||
|
m_pushUri = pushUri;
|
||||||
|
if (string.IsNullOrEmpty(pushUri) && ApplicationData.Current.LocalSettings.Values.ContainsKey(PushUriKey))
|
||||||
|
{
|
||||||
|
m_pushUri = ApplicationData.Current.LocalSettings.Values[PushUriKey] as string;
|
||||||
|
}
|
||||||
|
m_accoutProvider = accountProvider;
|
||||||
|
accountProvider.SignOutCompleted += (s, e) => Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetNotificationRegistration()
|
||||||
|
{
|
||||||
|
return m_pushUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
IAsyncOperation<string> IConnectedDevicesNotificationProvider.GetNotificationRegistrationAsync()
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Push registration requested by platform");
|
||||||
|
return GetNotificationRegistration().AsAsyncOperation();
|
||||||
|
}
|
||||||
|
|
||||||
|
event TypedEventHandler<IConnectedDevicesNotificationProvider, ConnectedDevicesNotificationRegistrationUpdatedEventArgs> IConnectedDevicesNotificationProvider.RegistrationUpdated
|
||||||
|
{
|
||||||
|
add { return new EventRegistrationToken(); }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Refresh()
|
||||||
|
{
|
||||||
|
await SetupChannel();
|
||||||
|
if (m_reader != null)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage("Read cached notifications");
|
||||||
|
ReadNotifications(m_reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.Instance.LogMessage("Request another sync");
|
||||||
|
m_feed?.StartSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ReceiveNotification(string content)
|
||||||
|
{
|
||||||
|
await SetupChannel();
|
||||||
|
m_platform.ReceiveNotification(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Activate(string id, bool dismiss)
|
||||||
|
{
|
||||||
|
await SetupChannel();
|
||||||
|
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.UserActionState = dismiss ? UserNotificationUserActionState.Dismissed : UserNotificationUserActionState.Activated;
|
||||||
|
await notification.SaveAsync();
|
||||||
|
RemoveToastNotification(notification.Id);
|
||||||
|
Logger.Instance.LogMessage($"{notification.Id} is now DISMISSED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void MarkRead(string id)
|
||||||
|
{
|
||||||
|
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
notification.ReadState = UserNotificationReadState.Read;
|
||||||
|
await notification.SaveAsync();
|
||||||
|
Logger.Instance.LogMessage($"{notification.Id} is now READ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Delete(string id)
|
||||||
|
{
|
||||||
|
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
|
||||||
|
if (notification != null)
|
||||||
|
{
|
||||||
|
await m_channel?.DeleteUserNotificationAsync(notification.Id);
|
||||||
|
Logger.Instance.LogMessage($"{notification.Id} is now DELETED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Reset()
|
||||||
|
{
|
||||||
|
if (m_platform != null)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage("Shutting down platform");
|
||||||
|
await m_platform.ShutdownAsync();
|
||||||
|
m_platform = null;
|
||||||
|
m_feed = null;
|
||||||
|
m_newNotifications.Clear();
|
||||||
|
m_historicalNotifications.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheUpdated?.Invoke(this, new EventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetupChannel()
|
||||||
|
{
|
||||||
|
var account = m_accoutProvider.SignedInAccount;
|
||||||
|
if (account != null && m_platform == null)
|
||||||
|
{
|
||||||
|
m_platform = new ConnectedDevicesPlatform(m_accoutProvider, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_feed == null)
|
||||||
|
{
|
||||||
|
// Need to run UserDataFeed creation on a background thread
|
||||||
|
// because MSA/AAD token request might need to show UI.
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
lock (this)
|
||||||
|
{
|
||||||
|
if (account != null && m_feed == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
m_feed = new UserDataFeed(account, m_platform, "graphnotifications.sample.windows.com");
|
||||||
|
m_feed.SyncStatusChanged += Feed_SyncStatusChanged;
|
||||||
|
m_feed.AddSyncScopes(new List<IUserDataFeedSyncScope>
|
||||||
|
{
|
||||||
|
UserNotificationChannel.SyncScope
|
||||||
|
});
|
||||||
|
|
||||||
|
m_channel = new UserNotificationChannel(m_feed);
|
||||||
|
m_reader = m_channel.CreateReader();
|
||||||
|
m_reader.DataChanged += Reader_DataChanged;
|
||||||
|
|
||||||
|
Logger.Instance.LogMessage($"Setup feed for {account.Id} {account.Type}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Failed to setup UserNotificationChannel {ex.Message}");
|
||||||
|
m_feed = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ReadNotifications(UserNotificationReader reader)
|
||||||
|
{
|
||||||
|
var notifications = await reader.ReadBatchAsync(UInt32.MaxValue);
|
||||||
|
Logger.Instance.LogMessage($"Read {notifications.Count} notifications");
|
||||||
|
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage($"UserNotification: {notification.Id} Status: {notification.Status} ReadState: {notification.ReadState} UserActionState: {notification.UserActionState}");
|
||||||
|
|
||||||
|
if (notification.Status == UserNotificationStatus.Active)
|
||||||
|
{
|
||||||
|
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
|
||||||
|
if (notification.UserActionState == UserNotificationUserActionState.NoInteraction)
|
||||||
|
{
|
||||||
|
// Brand new notification, add to new
|
||||||
|
m_newNotifications.Add(notification);
|
||||||
|
Logger.Instance.LogMessage($"UserNotification not interacted: {notification.Id}");
|
||||||
|
if (!string.IsNullOrEmpty(notification.Content) && notification.ReadState != UserNotificationReadState.Read)
|
||||||
|
{
|
||||||
|
RemoveToastNotification(notification.Id);
|
||||||
|
ShowToastNotification(BuildToastNotification(notification.Id, notification.Content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RemoveToastNotification(notification.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
|
||||||
|
m_historicalNotifications.Insert(0, notification);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Historical notification is marked as deleted, remove from display
|
||||||
|
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
|
||||||
|
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
|
||||||
|
RemoveToastNotification(notification.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheUpdated?.Invoke(this, new EventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Feed_SyncStatusChanged(UserDataFeed sender, object args)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"SyncStatus is {sender.SyncStatus.ToString()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reader_DataChanged(UserNotificationReader sender, object args)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage("New notification available");
|
||||||
|
ReadNotifications(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Windows.UI.Notifications.ToastNotification BuildToastNotification(string notificationId, string notificationContent)
|
||||||
|
{
|
||||||
|
XmlDocument toastXml = Windows.UI.Notifications.ToastNotificationManager.GetTemplateContent(Windows.UI.Notifications.ToastTemplateType.ToastText02);
|
||||||
|
XmlNodeList toastNodeList = toastXml.GetElementsByTagName("text");
|
||||||
|
toastNodeList.Item(0).AppendChild(toastXml.CreateTextNode(notificationId));
|
||||||
|
toastNodeList.Item(1).AppendChild(toastXml.CreateTextNode(notificationContent));
|
||||||
|
IXmlNode toastNode = toastXml.SelectSingleNode("/toast");
|
||||||
|
((XmlElement)toastNode).SetAttribute("launch", "{\"type\":\"toast\",\"notificationId\":\"" + notificationId + "\"}");
|
||||||
|
XmlElement audio = toastXml.CreateElement("audio");
|
||||||
|
audio.SetAttribute("src", "ms-winsoundevent:Notification.SMS");
|
||||||
|
return new Windows.UI.Notifications.ToastNotification(toastXml)
|
||||||
|
{
|
||||||
|
Tag = notificationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raise a new toast with UserNotification.Id as tag
|
||||||
|
private void ShowToastNotification(Windows.UI.Notifications.ToastNotification toast)
|
||||||
|
{
|
||||||
|
var toastNotifier = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
|
||||||
|
toast.Activated += (s, e) => Activate(s.Tag, false);
|
||||||
|
toastNotifier.Show(toast);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a toast with UserNotification.Id as tag
|
||||||
|
private void RemoveToastNotification(string notificationId)
|
||||||
|
{
|
||||||
|
Windows.UI.Notifications.ToastNotificationManager.History.Remove(notificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
|
||||||
|
<ProjectGuid>{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}</ProjectGuid>
|
||||||
|
<OutputType>AppContainerExe</OutputType>
|
||||||
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
|
<RootNamespace>SDKTemplate</RootNamespace>
|
||||||
|
<AssemblyName>GraphNotificationsSample</AssemblyName>
|
||||||
|
<DefaultLanguage>en-US</DefaultLanguage>
|
||||||
|
<TargetPlatformIdentifier>UAP</TargetPlatformIdentifier>
|
||||||
|
<TargetPlatformVersion>10.0.17134.0</TargetPlatformVersion>
|
||||||
|
<TargetPlatformMinVersion>10.0.15063.0</TargetPlatformMinVersion>
|
||||||
|
<MinimumVisualStudioVersion>14</MinimumVisualStudioVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||||
|
<RuntimeIdentifiers>win10-arm;win10-arm-aot;win10-x86;win10-x86-aot;win10-x64;win10-x64-aot</RuntimeIdentifiers>
|
||||||
|
<GenerateAppInstallerFile>False</GenerateAppInstallerFile>
|
||||||
|
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
|
||||||
|
<AppxBundle>Always</AppxBundle>
|
||||||
|
<AppxBundlePlatforms>x64</AppxBundlePlatforms>
|
||||||
|
<PackageCertificateThumbprint>3FD265E4FC786A7A28192B070A81132FF51AA0CE</PackageCertificateThumbprint>
|
||||||
|
<PackageCertificateKeyFile>GraphNotificationsSample_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||||
|
<AppInstallerUpdateFrequency>1</AppInstallerUpdateFrequency>
|
||||||
|
<AppInstallerCheckForUpdateFrequency>OnApplicationRun</AppInstallerCheckForUpdateFrequency>
|
||||||
|
<AppxSymbolPackageEnabled>False</AppxSymbolPackageEnabled>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|ARM'">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<OutputPath>bin\ARM\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<PlatformTarget>ARM</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|ARM'">
|
||||||
|
<OutputPath>bin\ARM\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<PlatformTarget>ARM</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<OutputPath>bin\x64\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||||
|
<OutputPath>bin\x64\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<PlatformTarget>x64</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<OutputPath>bin\x86\Debug\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'">
|
||||||
|
<OutputPath>bin\x86\Release\</OutputPath>
|
||||||
|
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<NoWarn>;2008</NoWarn>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<PlatformTarget>x86</PlatformTarget>
|
||||||
|
<UseVSHostingProcess>false</UseVSHostingProcess>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<Prefer32Bit>true</Prefer32Bit>
|
||||||
|
<UseDotNetNativeToolchain>true</UseDotNetNativeToolchain>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="LogsPage.xaml.cs">
|
||||||
|
<DependentUpon>LogsPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="GraphNotificationProvider.cs" />
|
||||||
|
<Compile Include="Logger.cs" />
|
||||||
|
<Compile Include="NotificationsPage.xaml.cs">
|
||||||
|
<DependentUpon>NotificationsPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="App.xaml.cs">
|
||||||
|
<DependentUpon>App.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="MainPage.xaml.cs">
|
||||||
|
<DependentUpon>MainPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="SampleConfiguration.cs" />
|
||||||
|
<Compile Include="AccountsPage.xaml.cs">
|
||||||
|
<DependentUpon>AccountsPage.xaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="MicrosoftAccountProvider.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<AppxManifest Include="Package.appxmanifest">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</AppxManifest>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ApplicationDefinition Include="App.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</ApplicationDefinition>
|
||||||
|
<Page Include="LogsPage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="MainPage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="NotificationsPage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="AccountsPage.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Include="Styles\Styles.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\microsoft-sdk.png" />
|
||||||
|
<Content Include="Assets\placeholder-sdk.png" />
|
||||||
|
<Content Include="Assets\placeholder.png" />
|
||||||
|
<Content Include="Assets\smalltile-sdk.png" />
|
||||||
|
<Content Include="Assets\splash-sdk.png" />
|
||||||
|
<Content Include="Assets\squaretile-sdk.png" />
|
||||||
|
<Content Include="Assets\storelogo-sdk.png" />
|
||||||
|
<Content Include="Assets\tile-sdk.png" />
|
||||||
|
<Content Include="Assets\windows-sdk.png" />
|
||||||
|
<None Include="Package.StoreAssociation.xml" />
|
||||||
|
<Content Include="Properties\Default.rd.xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.ConnectedDevices.UserNotifications.Uwp">
|
||||||
|
<Version>0.13.6</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
|
||||||
|
<Version>3.19.8</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
|
||||||
|
<Version>6.2.0-Preview1-26502-02</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Newtonsoft.Json">
|
||||||
|
<Version>11.0.2</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.Security.Cryptography.Algorithms">
|
||||||
|
<Version>4.3.1</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Xamarin.Auth">
|
||||||
|
<Version>1.6.0.2</Version>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="GraphNotificationsSample_TemporaryKey.pfx" />
|
||||||
|
</ItemGroup>
|
||||||
|
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||||
|
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SignAssembly>false</SignAssembly>
|
||||||
|
</PropertyGroup>
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
|
||||||
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
<Target Name="BeforeBuild">
|
||||||
|
</Target>
|
||||||
|
<Target Name="AfterBuild">
|
||||||
|
</Target>
|
||||||
|
-->
|
||||||
|
</Project>
|
|
@ -0,0 +1,40 @@
|
||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 14
|
||||||
|
VisualStudioVersion = 14.0.22609.0
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphNotificationsSample", "GraphNotificationsSample.csproj", "{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|ARM = Debug|ARM
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
|
Release|ARM = Release|ARM
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|ARM.ActiveCfg = Debug|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|ARM.Build.0 = Debug|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|ARM.Deploy.0 = Debug|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x64.Deploy.0 = Debug|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x86.ActiveCfg = Debug|x86
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x86.Build.0 = Debug|x86
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Debug|x86.Deploy.0 = Debug|x86
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|ARM.ActiveCfg = Release|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|ARM.Build.0 = Release|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|ARM.Deploy.0 = Release|ARM
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x64.Build.0 = Release|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x64.Deploy.0 = Release|x64
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x86.ActiveCfg = Release|x86
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x86.Build.0 = Release|x86
|
||||||
|
{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}.Release|x86.Deploy.0 = Release|x86
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
|
@ -0,0 +1,42 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
public static Logger Instance { get; } = new Logger();
|
||||||
|
|
||||||
|
public delegate void LogEventHandler(object sender, string message);
|
||||||
|
public event LogEventHandler LogUpdated;
|
||||||
|
|
||||||
|
public string AppLogs
|
||||||
|
{
|
||||||
|
get; set;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger()
|
||||||
|
{
|
||||||
|
AppLogs = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogMessage(string message)
|
||||||
|
{
|
||||||
|
message = $"[{string.Format("{0:T}", DateTime.Now)}] {message}";
|
||||||
|
Debug.WriteLine(message);
|
||||||
|
AppLogs = message + Environment.NewLine + AppLogs;
|
||||||
|
LogUpdated?.Invoke(this, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<Page
|
||||||
|
x:Class="SDKTemplate.LogsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="12,20,12,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||||
|
<TextBlock Name="Description" Style="{StaticResource ScenarioDescriptionTextStyle}" TextWrapping="Wrap">
|
||||||
|
Please login with MSA or AAD account
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10">
|
||||||
|
<Button Content="Clear" Margin="10" BorderBrush="AntiqueWhite" Click="Button_Clear"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="2" VerticalScrollMode="Auto" VerticalScrollBarVisibility="Auto">
|
||||||
|
<Border BorderBrush="AntiqueWhite" BorderThickness="2" Margin="20,0,0,0" >
|
||||||
|
<TextBox x:Name="LogView" Foreground="Red" AcceptsReturn="True" IsReadOnly="True" TextWrapping="Wrap"/>
|
||||||
|
</Border>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Status Block for providing messages to the user. Use the
|
||||||
|
NotifyUser() method to populate the message -->
|
||||||
|
<TextBlock x:Name="StatusBlock" Grid.Row="3" Margin="12, 10, 12, 10" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
|
@ -0,0 +1,67 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Windows.UI.Core;
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Windows.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public sealed partial class LogsPage : Page
|
||||||
|
{
|
||||||
|
private MainPage rootPage;
|
||||||
|
|
||||||
|
public LogsPage()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
rootPage = MainPage.Current;
|
||||||
|
|
||||||
|
var accountProvider = ((App)Application.Current).AccountProvider;
|
||||||
|
if (accountProvider.SignedInAccount != null)
|
||||||
|
{
|
||||||
|
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
|
||||||
|
if (accountProvider.AadUser != null)
|
||||||
|
{
|
||||||
|
Description.Text += accountProvider.AadUser.DisplayableId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LogView.Text = Logger.Instance.AppLogs;
|
||||||
|
Logger.Instance.LogUpdated += LogsUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogUpdated -= LogsUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void LogsUpdated(object sender, string message)
|
||||||
|
{
|
||||||
|
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
|
||||||
|
{
|
||||||
|
LogView.Text = message + Environment.NewLine + LogView.Text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_Clear(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
LogView.Text = string.Empty;
|
||||||
|
Logger.Instance.AppLogs = string.Empty;
|
||||||
|
rootPage.NotifyUser("Logs cleared", NotifyType.ErrorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<Page
|
||||||
|
x:Class="SDKTemplate.MainPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
x:Name="Main"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Page.Resources>
|
||||||
|
<local:ScenarioBindingConverter x:Key="ScenarioConverter"></local:ScenarioBindingConverter>
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<SplitView x:Name="Splitter" IsPaneOpen="True" Grid.Column="1" DisplayMode="Inline" Grid.Row="1">
|
||||||
|
<SplitView.Pane>
|
||||||
|
<RelativePanel Margin="10,0,0,0">
|
||||||
|
<TextBlock x:Name="SampleTitle" Text="Sample Title Here" Style="{StaticResource SampleHeaderTextStyle}" TextWrapping="Wrap" Margin="0,10,0,0"/>
|
||||||
|
<ListBox x:Name="ScenarioControl" SelectionChanged="ScenarioControl_SelectionChanged"
|
||||||
|
SelectionMode="Single" HorizontalAlignment="Left" Background="Transparent" BorderThickness="0"
|
||||||
|
VerticalAlignment="Top" RelativePanel.Below="SampleTitle" Margin="0,10,0,0" RelativePanel.Above="FooterPanel">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Converter={StaticResource ScenarioConverter}}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
<StackPanel x:Name="FooterPanel" Orientation="Vertical" RelativePanel.AlignBottomWithPanel="True">
|
||||||
|
<Image Source="Assets/microsoft-sdk.png" AutomationProperties.Name="Microsoft Logo" Stretch="None" HorizontalAlignment="Left" Margin="10,0,0,0"/>
|
||||||
|
<TextBlock x:Name="Copyright" Text="© Microsoft Corporation. All rights reserved." Style="{StaticResource CopyrightTextStyle}"
|
||||||
|
RelativePanel.Above="LinksPanel" Margin="10,10,0,0"
|
||||||
|
TextWrapping="Wrap"/>
|
||||||
|
<StackPanel x:Name="LinksPanel" Orientation="Horizontal" Margin="10,10,0,10">
|
||||||
|
<HyperlinkButton Content="Trademarks" Tag="http://go.microsoft.com/fwlink/?LinkID=623755"
|
||||||
|
Click="Footer_Click" FontSize="12" Style="{StaticResource HyperlinkStyle}" />
|
||||||
|
<TextBlock Text="|" Style="{StaticResource SeparatorStyle}" VerticalAlignment="Center" />
|
||||||
|
<HyperlinkButton x:Name="PrivacyLink" Content="Privacy" Tag="http://privacy.microsoft.com" Click="Footer_Click" FontSize="12" Style="{StaticResource HyperlinkStyle}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</RelativePanel>
|
||||||
|
</SplitView.Pane>
|
||||||
|
<RelativePanel>
|
||||||
|
<Frame x:Name="ScenarioFrame" Margin="0,5,0,0" RelativePanel.AlignTopWithPanel="True" RelativePanel.Above="StatusPanel" RelativePanel.AlignRightWithPanel="True" RelativePanel.AlignLeftWithPanel="True"/>
|
||||||
|
<StackPanel x:Name="StatusPanel" Orientation="Vertical" RelativePanel.AlignBottomWithPanel="True" RelativePanel.AlignRightWithPanel="True" RelativePanel.AlignLeftWithPanel="True">
|
||||||
|
<TextBlock x:Name="StatusLabel" Margin="10,0,0,10" TextWrapping="Wrap" Text="Status:" />
|
||||||
|
<Border x:Name="StatusBorder" Margin="0,0,0,0">
|
||||||
|
<ScrollViewer VerticalScrollMode="Auto" VerticalScrollBarVisibility="Auto" MaxHeight="200">
|
||||||
|
<TextBlock x:Name="StatusBlock" FontWeight="Bold"
|
||||||
|
MaxWidth="{Binding ElementName=Splitter, Path=ActualWidth}" Margin="10,10,10,20" TextWrapping="Wrap" />
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</RelativePanel>
|
||||||
|
</SplitView>
|
||||||
|
|
||||||
|
<StackPanel x:Name="HeaderPanel" Orientation="Horizontal">
|
||||||
|
<Border Background="{ThemeResource SystemControlBackgroundChromeMediumBrush}" Grid.Row="0">
|
||||||
|
<ToggleButton Style="{StaticResource SymbolButton}" Click="Button_Click" VerticalAlignment="Top" Foreground="{ThemeResource ApplicationForegroundThemeBrush}">
|
||||||
|
<ToggleButton.Content>
|
||||||
|
<FontIcon x:Name="Hamburger" FontFamily="Segoe MDL2 Assets" Glyph="" Margin="0,10,0,0"/>
|
||||||
|
</ToggleButton.Content>
|
||||||
|
</ToggleButton>
|
||||||
|
</Border>
|
||||||
|
<Image x:Name="WindowsLogo" Stretch="None" Source="Assets/windows-sdk.png" Margin="0,15,0,0" />
|
||||||
|
<TextBlock x:Name="Header" Text="Universal Windows Platform sample" Style="{StaticResource TagLineTextStyle}" Margin="0,15,0,0" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
|
@ -0,0 +1,201 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Windows.ApplicationModel.Background;
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Windows.UI.Xaml.Data;
|
||||||
|
using Windows.UI.Xaml.Media;
|
||||||
|
using Windows.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
// The Blank Page item template is documented at http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An empty page that can be used on its own or navigated to within a Frame.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class MainPage : Page
|
||||||
|
{
|
||||||
|
public static MainPage Current;
|
||||||
|
static readonly string PushTaskName = "GraphNotificationsPush";
|
||||||
|
|
||||||
|
public MainPage()
|
||||||
|
{
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
// This is a static public property that allows downstream pages to get a handle to the MainPage instance
|
||||||
|
// in order to call methods that are in this class.
|
||||||
|
Current = this;
|
||||||
|
SampleTitle.Text = FEATURE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnNavigatedTo(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
// Applications must have lock screen privileges in order to receive raw notifications
|
||||||
|
BackgroundAccessStatus backgroundStatus = await BackgroundExecutionManager.RequestAccessAsync();
|
||||||
|
|
||||||
|
// Make sure the user allowed privileges
|
||||||
|
if (backgroundStatus != BackgroundAccessStatus.DeniedByUser &&
|
||||||
|
backgroundStatus != BackgroundAccessStatus.DeniedBySystemPolicy &&
|
||||||
|
backgroundStatus != BackgroundAccessStatus.Unspecified)
|
||||||
|
{
|
||||||
|
RegisterBackgroundTask();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NotifyUser("Background access is denied", NotifyType.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the scenario list from the SampleConfiguration.cs file
|
||||||
|
ScenarioControl.ItemsSource = scenarios;
|
||||||
|
if (Window.Current.Bounds.Width < 640)
|
||||||
|
{
|
||||||
|
ScenarioControl.SelectedIndex = -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ScenarioControl.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterBackgroundTask()
|
||||||
|
{
|
||||||
|
// Unregister first
|
||||||
|
UnregisterBackgroundTask();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BackgroundTaskBuilder taskBuilder = new BackgroundTaskBuilder
|
||||||
|
{
|
||||||
|
Name = PushTaskName
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do not set BackgroundTaskBuilder.TaskEntryPoint for in-process background tasks
|
||||||
|
// Here we register the task and work will start based on the time trigger.
|
||||||
|
|
||||||
|
taskBuilder.SetTrigger(new PushNotificationTrigger());
|
||||||
|
|
||||||
|
BackgroundTaskRegistration task = taskBuilder.Register();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
NotifyUser("BackgroundTaskRegistration error: " + ex.Message, NotifyType.ErrorMessage);
|
||||||
|
UnregisterBackgroundTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UnregisterBackgroundTask()
|
||||||
|
{
|
||||||
|
foreach (var iter in BackgroundTaskRegistration.AllTasks)
|
||||||
|
{
|
||||||
|
IBackgroundTaskRegistration task = iter.Value;
|
||||||
|
if (task.Name == PushTaskName)
|
||||||
|
{
|
||||||
|
task.Unregister(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called whenever the user changes selection in the scenarios list. This method will navigate to the respective
|
||||||
|
/// sample scenario page.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sender"></param>
|
||||||
|
/// <param name="e"></param>
|
||||||
|
private void ScenarioControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// Clear the status block when navigating scenarios.
|
||||||
|
NotifyUser(String.Empty, NotifyType.StatusMessage);
|
||||||
|
|
||||||
|
ListBox scenarioListBox = sender as ListBox;
|
||||||
|
Scenario s = scenarioListBox.SelectedItem as Scenario;
|
||||||
|
if (s != null)
|
||||||
|
{
|
||||||
|
ScenarioFrame.Navigate(s.ClassType);
|
||||||
|
if (Window.Current.Bounds.Width < 640)
|
||||||
|
{
|
||||||
|
Splitter.IsPaneOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Scenario> Scenarios
|
||||||
|
{
|
||||||
|
get { return this.scenarios; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to display messages to the user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="strMessage"></param>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
public void NotifyUser(string strMessage, NotifyType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case NotifyType.StatusMessage:
|
||||||
|
StatusBorder.Background = new SolidColorBrush(Windows.UI.Colors.Green);
|
||||||
|
break;
|
||||||
|
case NotifyType.ErrorMessage:
|
||||||
|
StatusBorder.Background = new SolidColorBrush(Windows.UI.Colors.Red);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
StatusBlock.Text = strMessage;
|
||||||
|
|
||||||
|
// Collapse the StatusBlock if it has no text to conserve real estate.
|
||||||
|
StatusBorder.Visibility = (StatusBlock.Text != String.Empty) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
if (StatusBlock.Text != String.Empty)
|
||||||
|
{
|
||||||
|
StatusBorder.Visibility = Visibility.Visible;
|
||||||
|
StatusPanel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StatusBorder.Visibility = Visibility.Collapsed;
|
||||||
|
StatusPanel.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async void Footer_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await Windows.System.Launcher.LaunchUriAsync(new Uri(((HyperlinkButton)sender).Tag.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Splitter.IsPaneOpen = !Splitter.IsPaneOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public enum NotifyType
|
||||||
|
{
|
||||||
|
StatusMessage,
|
||||||
|
ErrorMessage
|
||||||
|
};
|
||||||
|
|
||||||
|
public class ScenarioBindingConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
Scenario s = value as Scenario;
|
||||||
|
return (MainPage.Current.Scenarios.IndexOf(s) + 1) + ") " + s.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,279 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using Microsoft.ConnectedDevices.Core;
|
||||||
|
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.Foundation;
|
||||||
|
using Windows.Security.Authentication.Web;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Xamarin.Auth;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public class MicrosoftAccountProvider : IConnectedDevicesUserAccountProvider
|
||||||
|
{
|
||||||
|
static readonly string CCSResource = "https://cdpcs.access.microsoft.com";
|
||||||
|
|
||||||
|
static readonly string MsaTokenKey = "MsaToken";
|
||||||
|
|
||||||
|
public event EventHandler SignOutCompleted;
|
||||||
|
public ConnectedDevicesUserAccount SignedInAccount { get; set; }
|
||||||
|
public string MsaToken { get; set; }
|
||||||
|
public UserInfo AadUser { get; set; }
|
||||||
|
|
||||||
|
public MicrosoftAccountProvider()
|
||||||
|
{
|
||||||
|
if (ApplicationData.Current.LocalSettings.Values.ContainsKey(MsaTokenKey))
|
||||||
|
{
|
||||||
|
MsaToken = ApplicationData.Current.LocalSettings.Values[MsaTokenKey] as string;
|
||||||
|
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.MSA);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
|
||||||
|
if (authContext.TokenCache.Count > 0)
|
||||||
|
{
|
||||||
|
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.AAD);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SignInAad()
|
||||||
|
{
|
||||||
|
var result = await GetAadTokenForUserAsync(CCSResource);
|
||||||
|
if (result.TokenRequestStatus == ConnectedDevicesAccessTokenRequestStatus.Success)
|
||||||
|
{
|
||||||
|
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.AAD);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> SignInMsa()
|
||||||
|
{
|
||||||
|
string refreshToken = string.Empty;
|
||||||
|
if (ApplicationData.Current.LocalSettings.Values.ContainsKey(MsaTokenKey))
|
||||||
|
{
|
||||||
|
refreshToken = ApplicationData.Current.LocalSettings.Values[MsaTokenKey] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(refreshToken))
|
||||||
|
{
|
||||||
|
refreshToken = await MSAOAuthHelpers.GetRefreshTokenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(refreshToken))
|
||||||
|
{
|
||||||
|
MsaToken = refreshToken;
|
||||||
|
ApplicationData.Current.LocalSettings.Values[MsaTokenKey] = refreshToken;
|
||||||
|
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.MSA);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SignOut()
|
||||||
|
{
|
||||||
|
MsaToken = string.Empty;
|
||||||
|
AadUser = null;
|
||||||
|
SignedInAccount = null;
|
||||||
|
ApplicationData.Current.LocalSettings.Values.Remove(MsaTokenKey);
|
||||||
|
new AuthenticationContext("https://login.microsoftonline.com/common").TokenCache.Clear();
|
||||||
|
SignOutCompleted?.Invoke(null, new EventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
IAsyncOperation<ConnectedDevicesAccessTokenResult> IConnectedDevicesUserAccountProvider.GetAccessTokenForUserAccountAsync(string userAccountId, IReadOnlyList<string> scopes)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Token requested by platform for {userAccountId} and {string.Join(" ", scopes)}");
|
||||||
|
if (SignedInAccount.Type == ConnectedDevicesUserAccountType.AAD)
|
||||||
|
{
|
||||||
|
return GetAadTokenForUserAsync(scopes.First()).AsAsyncOperation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return GetMsaTokenForUserAsync(scopes).AsAsyncOperation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IConnectedDevicesUserAccountProvider.OnAccessTokenError(string userAccountId, IReadOnlyList<string> scopes, bool isPermanentError)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"Bad token reported for {userAccountId} isPermanentError: {isPermanentError}");
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyList<ConnectedDevicesUserAccount> IConnectedDevicesUserAccountProvider.UserAccounts
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var accounts = new List<ConnectedDevicesUserAccount>();
|
||||||
|
var account = SignedInAccount;
|
||||||
|
if (account != null)
|
||||||
|
{
|
||||||
|
accounts.Add(account);
|
||||||
|
}
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event TypedEventHandler<IConnectedDevicesUserAccountProvider, object> IConnectedDevicesUserAccountProvider.UserAccountChanged
|
||||||
|
{
|
||||||
|
add { return new EventRegistrationToken(); }
|
||||||
|
remove { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConnectedDevicesAccessTokenResult> GetAadTokenForUserAsync(string audience)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
|
||||||
|
AuthenticationResult result = await authContext.AcquireTokenAsync(
|
||||||
|
audience, Secrets.AAD_CLIENT_ID, new Uri(Secrets.AAD_REDIRECT_URI), new PlatformParameters(PromptBehavior.Auto, true));
|
||||||
|
if (AadUser == null)
|
||||||
|
{
|
||||||
|
AadUser = result.UserInfo;
|
||||||
|
Logger.Instance.LogMessage($"SignIn done for {AadUser.DisplayableId}");
|
||||||
|
}
|
||||||
|
Logger.Instance.LogMessage($"AAD Token : {result.AccessToken}");
|
||||||
|
return new ConnectedDevicesAccessTokenResult(result.AccessToken, ConnectedDevicesAccessTokenRequestStatus.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"AAD Token request failed: {ex.Message}");
|
||||||
|
return new ConnectedDevicesAccessTokenResult(string.Empty, ConnectedDevicesAccessTokenRequestStatus.TransientError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ConnectedDevicesAccessTokenResult> GetMsaTokenForUserAsync(IReadOnlyList<string> scopes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string accessToken = await MSAOAuthHelpers.GetAccessTokenUsingRefreshTokenAsync(MsaToken, scopes);
|
||||||
|
Logger.Instance.LogMessage($"MSA Token : {accessToken}");
|
||||||
|
return new ConnectedDevicesAccessTokenResult(accessToken, ConnectedDevicesAccessTokenRequestStatus.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage($"MSA Token request failed: {ex.Message}");
|
||||||
|
return new ConnectedDevicesAccessTokenResult(string.Empty, ConnectedDevicesAccessTokenRequestStatus.TransientError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MSAOAuthHelpers
|
||||||
|
{
|
||||||
|
static readonly string ProdAuthorizeUrl = "https://login.live.com/oauth20_authorize.srf";
|
||||||
|
static readonly string ProdRedirectUrl = "https://login.microsoftonline.com/common/oauth2/nativeclient";
|
||||||
|
static readonly string ProdAccessTokenUrl = "https://login.live.com/oauth20_token.srf";
|
||||||
|
|
||||||
|
static readonly string OfflineAccessScope = "wl.offline_access";
|
||||||
|
static readonly string CCSScope = "ccs.ReadWrite";
|
||||||
|
static readonly string UserActivitiesScope = "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp";
|
||||||
|
static readonly string UserNotificationsScope = "https://activity.windows.com/Notifications.ReadWrite.CreatedByApp";
|
||||||
|
|
||||||
|
static Random Randomizer = new Random((int)DateTime.Now.Ticks);
|
||||||
|
static SHA256 HashProvider = SHA256.Create();
|
||||||
|
|
||||||
|
static async Task<IDictionary<string, string>> RequestAccessTokenAsync(string accessTokenUrl, IDictionary<string, string> queryValues)
|
||||||
|
{
|
||||||
|
var content = new FormUrlEncodedContent(queryValues);
|
||||||
|
|
||||||
|
HttpClient client = new HttpClient();
|
||||||
|
HttpResponseMessage response = await client.PostAsync(accessTokenUrl, content).ConfigureAwait(false);
|
||||||
|
string text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Parse the response
|
||||||
|
IDictionary<string, string> data = text.Contains("{") ? WebEx.JsonDecode(text) : WebEx.FormDecode(text);
|
||||||
|
if (data.ContainsKey("error"))
|
||||||
|
{
|
||||||
|
throw new AuthException(data["error_description"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetRefreshTokenAsync()
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[32];
|
||||||
|
Randomizer.NextBytes(buffer);
|
||||||
|
var codeVerifier = Convert.ToBase64String(buffer).Replace('+', '-').Replace('/', '_').Replace("=", "");
|
||||||
|
|
||||||
|
byte[] hash = HashProvider.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
|
||||||
|
var codeChallenge = Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').Replace("=", "");
|
||||||
|
|
||||||
|
var redirectUri = new Uri(ProdRedirectUrl);
|
||||||
|
|
||||||
|
string scope = $"{OfflineAccessScope} {CCSScope} {UserNotificationsScope} {UserActivitiesScope}";
|
||||||
|
var startUri = new Uri($"{ProdAuthorizeUrl}?client_id={Secrets.MSA_CLIENT_ID}&response_type=code&code_challenge_method=S256&code_challenge={codeChallenge}&redirect_uri={ProdRedirectUrl}&scope={scope}");
|
||||||
|
|
||||||
|
var webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
|
||||||
|
WebAuthenticationOptions.None,
|
||||||
|
startUri,
|
||||||
|
redirectUri);
|
||||||
|
|
||||||
|
if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
|
||||||
|
{
|
||||||
|
var codeResponseUri = new Uri(webAuthenticationResult.ResponseData);
|
||||||
|
IDictionary<string, string> queryParams = WebEx.FormDecode(codeResponseUri.Query);
|
||||||
|
if (!queryParams.ContainsKey("code"))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
string authCode = queryParams["code"];
|
||||||
|
Dictionary<string, string> refreshTokenQuery = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", ProdClientId },
|
||||||
|
{ "redirect_uri", redirectUri.AbsoluteUri },
|
||||||
|
{ "grant_type", "authorization_code" },
|
||||||
|
{ "code", authCode },
|
||||||
|
{ "code_verifier", codeVerifier },
|
||||||
|
{ "scope", CCSScope }
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string> refreshTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, refreshTokenQuery);
|
||||||
|
if (refreshTokenResponse.ContainsKey("refresh_token"))
|
||||||
|
{
|
||||||
|
return refreshTokenResponse["refresh_token"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<string> GetAccessTokenUsingRefreshTokenAsync(string refreshToken, IReadOnlyList<string> scopes)
|
||||||
|
{
|
||||||
|
Dictionary<string, string> accessTokenQuery = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "client_id", ProdClientId },
|
||||||
|
{ "redirect_uri", ProdRedirectUrl },
|
||||||
|
{ "grant_type", "refresh_token" },
|
||||||
|
{ "refresh_token", refreshToken },
|
||||||
|
{ "scope", string.Join(" ", scopes.ToArray()) },
|
||||||
|
};
|
||||||
|
|
||||||
|
IDictionary<string, string> accessTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, accessTokenQuery);
|
||||||
|
if (accessTokenResponse == null || !accessTokenResponse.ContainsKey("access_token"))
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to fetch access_token!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessTokenResponse["access_token"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<Page
|
||||||
|
x:Class="SDKTemplate.NotificationsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Page.Resources>
|
||||||
|
<local:BoolColorConverter x:Key="BoolColorConverter"/>
|
||||||
|
</Page.Resources>
|
||||||
|
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||||
|
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="12,20,12,12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<StackPanel HorizontalAlignment="Left" VerticalAlignment="Top">
|
||||||
|
<TextBlock Name="Description" Style="{StaticResource ScenarioDescriptionTextStyle}" TextWrapping="Wrap">
|
||||||
|
Please login with MSA or AAD account
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10">
|
||||||
|
<Button Name="RefreshButton" Content="Retrieve Notifications History" Margin="10" BorderBrush="AntiqueWhite" Click="Button_Refresh"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid Grid.Row="2">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Visible">
|
||||||
|
<ListView x:Name="UnreadView" Margin="20,0,0,0" BorderBrush="DarkCyan" BorderThickness="1">
|
||||||
|
<ListView.HeaderTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Margin="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0" Width="150" BorderBrush="DarkCyan" BorderThickness="2">
|
||||||
|
<TextBlock Margin="10" Text="Click" FontSize="18" FontWeight="ExtraBold" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="1" Width="100" BorderBrush="DarkCyan" BorderThickness="2">
|
||||||
|
<TextBlock Margin="10" Text="Click" FontSize="18" FontWeight="ExtraBold" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="2" Width="200" BorderBrush="DarkCyan" BorderThickness="2">
|
||||||
|
<TextBlock Margin="10" Text="UserActionState" FontSize="18" FontWeight="ExtraBold" HorizontalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="3" Width="400" BorderBrush="DarkCyan" BorderThickness="2">
|
||||||
|
<TextBlock Margin="10" Text="Content" FontSize="18" FontWeight="ExtraBold" HorizontalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.HeaderTemplate>
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0" Width="150" BorderBrush="DarkCyan" BorderThickness="1">
|
||||||
|
<Button Margin="10" Content="Mark Read" HorizontalAlignment="Center" IsEnabled="{Binding UnreadState}" Click="Button_MarkRead"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="1" Width="100" BorderBrush="DarkCyan" BorderThickness="1">
|
||||||
|
<Button Margin="10" Content="Delete" HorizontalAlignment="Center" Click="Button_Delete"/>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="2" Width="200" BorderBrush="DarkCyan" BorderThickness="1">
|
||||||
|
<TextBlock Margin="10" HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding UserActionState}" />
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="3" Width="400" BorderBrush="DarkCyan" BorderThickness="1">
|
||||||
|
<StackPanel Orientation="Vertical" Margin="10" >
|
||||||
|
<TextBlock Text="{Binding Id}" FontWeight="Bold" TextWrapping="Wrap" Foreground="{Binding UnreadState, Converter={StaticResource BoolColorConverter}}"/>
|
||||||
|
<TextBlock Text="{Binding Content}" TextWrapping="Wrap"/>
|
||||||
|
<TextBlock Text="{Binding Priority}" />
|
||||||
|
<TextBlock Text="{Binding ChangeTime}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Status Block for providing messages to the user. Use the
|
||||||
|
NotifyUser() method to populate the message -->
|
||||||
|
<TextBlock x:Name="StatusBlock" Grid.Row="3" Margin="12, 10, 12, 10" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
|
@ -0,0 +1,137 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the Microsoft Public License.
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using Microsoft.ConnectedDevices.UserNotifications;
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Windows.UI;
|
||||||
|
using Windows.UI.Core;
|
||||||
|
using Windows.UI.Xaml;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
using Windows.UI.Xaml.Data;
|
||||||
|
using Windows.UI.Xaml.Media;
|
||||||
|
using Windows.UI.Xaml.Navigation;
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
class NotificationListItem
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Content { get; set; }
|
||||||
|
public bool UnreadState { get; set; }
|
||||||
|
public string UserActionState { get; set; }
|
||||||
|
public string Priority { get; set; }
|
||||||
|
public string ExpirationTime { get; set; }
|
||||||
|
public string ChangeTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BoolColorConverter : IValueConverter
|
||||||
|
{
|
||||||
|
object IValueConverter.Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
return new SolidColorBrush(((bool)value) ? Colors.Green : Colors.Red);
|
||||||
|
}
|
||||||
|
|
||||||
|
object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class NotificationsPage : Page
|
||||||
|
{
|
||||||
|
private MainPage rootPage;
|
||||||
|
private ObservableCollection<NotificationListItem> activeNotifications = new ObservableCollection<NotificationListItem>();
|
||||||
|
private GraphNotificationProvider notificationCache;
|
||||||
|
|
||||||
|
public NotificationsPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
UnreadView.ItemsSource = activeNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
rootPage = MainPage.Current;
|
||||||
|
var accountProvider = ((App)Application.Current).AccountProvider;
|
||||||
|
RefreshButton.IsEnabled = (accountProvider.SignedInAccount != null);
|
||||||
|
if (accountProvider.SignedInAccount != null)
|
||||||
|
{
|
||||||
|
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
|
||||||
|
if (accountProvider.AadUser != null)
|
||||||
|
{
|
||||||
|
Description.Text += accountProvider.AadUser.DisplayableId;
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationCache = ((App)Application.Current).NotificationProvider;
|
||||||
|
notificationCache.CacheUpdated += Cache_CacheUpdated;
|
||||||
|
notificationCache.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnNavigatedFrom(NavigationEventArgs e)
|
||||||
|
{
|
||||||
|
if (notificationCache != null)
|
||||||
|
{
|
||||||
|
notificationCache.CacheUpdated -= Cache_CacheUpdated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Cache_CacheUpdated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
|
||||||
|
{
|
||||||
|
activeNotifications.Clear();
|
||||||
|
foreach (UserNotification notification in notificationCache.HistoricalNotifications)
|
||||||
|
{
|
||||||
|
activeNotifications.Add(new NotificationListItem()
|
||||||
|
{
|
||||||
|
Id = notification.Id,
|
||||||
|
Content = $" Content:{notification.Content}",
|
||||||
|
UnreadState = notification.ReadState == UserNotificationReadState.Unread,
|
||||||
|
UserActionState = notification.UserActionState.ToString(),
|
||||||
|
Priority = $" Priority: {notification.Priority.ToString()}",
|
||||||
|
ExpirationTime = $" Expiry: {notification.ExpirationTime.ToLocalTime().ToString()}",
|
||||||
|
ChangeTime = $" Last Updated: {notification.ChangeTime.ToLocalTime().ToString()}",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationCache.NewNotifications)
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("History is up-to-date. New notifications available", NotifyType.StatusMessage);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("History is up-to-date", NotifyType.StatusMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_Refresh(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
rootPage.NotifyUser("Updating history", NotifyType.StatusMessage);
|
||||||
|
notificationCache.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_MarkRead(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var item = ((Grid)((Border)((Button)sender).Parent).Parent).DataContext as NotificationListItem;
|
||||||
|
notificationCache.MarkRead(item.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_Delete(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var item = ((Grid)((Border)((Button)sender).Parent).Parent).DataContext as NotificationListItem;
|
||||||
|
notificationCache.Delete(item.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" IgnorableNamespaces="uap mp">
|
||||||
|
<Identity Name="8f92a39d-5520-48d0-a4d3-e35ad84d19a7" Publisher="CN=8f92a39d-5520-48d0-a4d3-e35ad84d19a7" Version="1.0.0.0" />
|
||||||
|
<mp:PhoneIdentity PhoneProductId="8f92a39d-5520-48d0-a4d3-e35ad84d19a7" PhonePublisherId="00000000-0000-0000-0000-000000000000" />
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>Graph Notifications Sample</DisplayName>
|
||||||
|
<PublisherDisplayName>Graph Notifications</PublisherDisplayName>
|
||||||
|
<Logo>Assets\StoreLogo-sdk.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.10240.0" MaxVersionTested="10.0.10586.0" />
|
||||||
|
</Dependencies>
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="x-generate" />
|
||||||
|
</Resources>
|
||||||
|
<Applications>
|
||||||
|
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="SDKTemplate.App">
|
||||||
|
<uap:VisualElements DisplayName="Graph Notifications Sample App" Square150x150Logo="Assets\SquareTile-sdk.png" Square44x44Logo="Assets\SmallTile-sdk.png" Description="Sample for GraphNotifications UWP SDK" BackgroundColor="#00b2f0">
|
||||||
|
<uap:SplashScreen BackgroundColor="#00b2f0" Image="Assets\Splash-sdk.png" />
|
||||||
|
<uap:DefaultTile>
|
||||||
|
<uap:ShowNameOnTiles>
|
||||||
|
<uap:ShowOn Tile="square150x150Logo" />
|
||||||
|
</uap:ShowNameOnTiles>
|
||||||
|
</uap:DefaultTile>
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
<Capabilities>
|
||||||
|
<Capability Name="internetClient" />
|
||||||
|
<Capability Name="privateNetworkClientServer" />
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
|
@ -0,0 +1,29 @@
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("GraphNotificationsSample")]
|
||||||
|
[assembly: AssemblyDescription("GraphNotifications C# Sample")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("Microsoft Corporation")]
|
||||||
|
[assembly: AssemblyProduct("Windows Samples")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © Microsoft Corporation")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
|
[assembly: ComVisible(false)]
|
|
@ -0,0 +1,31 @@
|
||||||
|
<!--
|
||||||
|
This file contains Runtime Directives used by .NET Native. The defaults here are suitable for most
|
||||||
|
developers. However, you can modify these parameters to modify the behavior of the .NET Native
|
||||||
|
optimizer.
|
||||||
|
|
||||||
|
Runtime Directives are documented at http://go.microsoft.com/fwlink/?LinkID=391919
|
||||||
|
|
||||||
|
To fully enable reflection for App1.MyClass and all of its public/private members
|
||||||
|
<Type Name="App1.MyClass" Dynamic="Required All"/>
|
||||||
|
|
||||||
|
To enable dynamic creation of the specific instantiation of AppClass<T> over System.Int32
|
||||||
|
<TypeInstantiation Name="App1.AppClass" Arguments="System.Int32" Activate="Required Public" />
|
||||||
|
|
||||||
|
Using the Namespace directive to apply reflection policy to all the types in a particular namespace
|
||||||
|
<Namespace Name="DataClasses.ViewModels" Seralize="All" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
|
||||||
|
<Application>
|
||||||
|
<!--
|
||||||
|
An Assembly element with Name="*Application*" applies to all assemblies in
|
||||||
|
the application package. The asterisks are not wildcards.
|
||||||
|
-->
|
||||||
|
<Assembly Name="*Application*" Dynamic="Required All" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Add your application specific runtime directives here. -->
|
||||||
|
|
||||||
|
|
||||||
|
</Application>
|
||||||
|
</Directives>
|
|
@ -0,0 +1,41 @@
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Windows.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
// Important Note for running this sample:
|
||||||
|
// The sample as-is will not be able to get tokens without having it's app manifest being
|
||||||
|
// modified to use the App Identity of a registered Microsoft Store/registered AAD app.
|
||||||
|
//
|
||||||
|
// See 'Related Topics' in the README.md for instructions on how to register an app.
|
||||||
|
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
public partial class MainPage : Page
|
||||||
|
{
|
||||||
|
public const string FEATURE_NAME = "Graph Notifications";
|
||||||
|
|
||||||
|
List<Scenario> scenarios = new List<Scenario>
|
||||||
|
{
|
||||||
|
new Scenario() { Title="Login/Logout", ClassType=typeof(AccountsPage)},
|
||||||
|
new Scenario() { Title="Notification History", ClassType=typeof(NotificationsPage)},
|
||||||
|
new Scenario() { Title="App Logs", ClassType=typeof(LogsPage)},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Scenario
|
||||||
|
{
|
||||||
|
public string Title { get; set; }
|
||||||
|
public Type ClassType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
namespace SDKTemplate
|
||||||
|
{
|
||||||
|
class Secrets
|
||||||
|
{
|
||||||
|
// These come from the converged app registration portal at apps.dev.microsoft.com
|
||||||
|
// MSA_CLIENT_ID: Id of this app's registration in the MSA portal
|
||||||
|
// AAD_CLIENT_ID: Id of this app's registration in the Azure portal
|
||||||
|
// AAD_REDIRECT_URI: A Uri that this app is registered with in the Azure portal.
|
||||||
|
// AAD is supposed to use this Uri to call the app back after login (currently not true, external requirement)
|
||||||
|
// And this app is supposed to be able to handle this Uri (currently not true)
|
||||||
|
// APP_HOST_NAME Cross-device domain of this app's registration
|
||||||
|
static readonly string MSA_CLIENT_ID = "<<MSA client ID goes here>>";
|
||||||
|
static readonly string AAD_CLIENT_ID = "<<AAD client ID goes here>>";
|
||||||
|
static readonly string AAD_REDIRECT_URI = "<<AAD redirect URI goes here>>";
|
||||||
|
static readonly string APP_HOST_NAME = "<<App cross-device domain goes here>>";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,536 @@
|
||||||
|
<!--
|
||||||
|
//*********************************************************
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft. All rights reserved.
|
||||||
|
// This code is licensed under the MIT License (MIT).
|
||||||
|
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
|
||||||
|
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
|
||||||
|
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
|
||||||
|
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
|
||||||
|
//
|
||||||
|
//*********************************************************
|
||||||
|
-->
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:SDKTemplate">
|
||||||
|
|
||||||
|
<Style x:Key="SymbolButton" TargetType="ToggleButton">
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource SymbolThemeFontFamily}" />
|
||||||
|
<Setter Property="MinHeight" Value="48" />
|
||||||
|
<Setter Property="MinWidth" Value="48" />
|
||||||
|
<Setter Property="Margin" Value="0,4,0,0" />
|
||||||
|
<Setter Property="Padding" Value="0" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
<Setter Property="VerticalAlignment" Value="Top" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseHighBrush}" />
|
||||||
|
<Setter Property="Content" Value="" />
|
||||||
|
<Setter Property="AutomationProperties.Name" Value="Menu" />
|
||||||
|
<Setter Property="UseSystemFocusVisuals" Value="True" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToggleButton">
|
||||||
|
<Grid x:Name="LayoutRoot"
|
||||||
|
Background="{TemplateBinding Background}">
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal" />
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Checked"/>
|
||||||
|
<VisualState x:Name="CheckedPointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="CheckedPressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(Grid.Background)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightAltBaseHighBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="CheckedDisabled">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(TextBlock.Foreground)">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlDisabledBaseLowBrush}"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
Margin="{TemplateBinding Padding}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
AutomationProperties.AccessibilityView="Raw" />
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="BasicTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||||
|
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="TagLineTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SampleHeaderTextStyle" TargetType="TextBlock" BasedOn="{StaticResource TitleTextBlockStyle}">
|
||||||
|
<Setter Property="FontSize" Value="28"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ListItemTextStyle" TargetType="TextBlock" BasedOn="{StaticResource SubtitleTextBlockStyle}">
|
||||||
|
<Setter Property="FontSize" Value="18"/>
|
||||||
|
<Setter Property="Margin" Value="10,0,0,0"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SystemControlForegroundBaseHighBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CopyrightTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
|
||||||
|
<Setter Property="FontWeight" Value="Normal"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ScenarioHeaderTextStyle" TargetType="TextBlock" BasedOn="{StaticResource TitleTextBlockStyle}">
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ScenarioDescriptionTextStyle" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="BaseMessageStyle" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
|
||||||
|
<Setter Property="Margin" Value="0,0,0,5"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SeparatorStyle" TargetType="TextBlock" BasedOn="{StaticResource BaseTextBlockStyle}">
|
||||||
|
<Setter Property="FontSize" Value="9"/>
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseMediumBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="HyperlinkStyle" TargetType="HyperlinkButton">
|
||||||
|
<Setter Property="Padding" Value="1"/>
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource SystemControlForegroundBaseMediumBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="NavMenuItemContainerStyle" TargetType="ListViewItem">
|
||||||
|
<Setter Property="MinWidth" Value="{StaticResource SplitViewCompactPaneThemeLength}"/>
|
||||||
|
<Setter Property="MinHeight" Value="48"/>
|
||||||
|
<Setter Property="Padding" Value="0"/>
|
||||||
|
<Setter Property="UseSystemFocusVisuals" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListViewItem">
|
||||||
|
<Grid x:Name="ContentBorder"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Control.IsTemplateFocusTarget="True"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}"
|
||||||
|
RenderTransformOrigin="0.5,0.5">
|
||||||
|
<Grid.RenderTransform>
|
||||||
|
<ScaleTransform x:Name="ContentBorderScale" />
|
||||||
|
</Grid.RenderTransform>
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal">
|
||||||
|
<Storyboard>
|
||||||
|
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="BorderBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="BorderBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<PointerDownThemeAnimation TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Selected">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="BorderBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="SelectedPipe"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlForegroundAccentBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PointerOverSelected">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="BorderBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="SelectedPipe"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlForegroundAccentBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<PointerUpThemeAnimation Storyboard.TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="PressedSelected">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="BorderBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="SelectedPipe"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="1"/>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BorderBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListMediumBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlForegroundAccentBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<PointerDownThemeAnimation TargetName="ContentPresenter" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="DisabledStates">
|
||||||
|
<VisualState x:Name="Enabled"/>
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="ContentBorder"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
Duration="0"
|
||||||
|
To="{ThemeResource ListViewItemDisabledThemeOpacity}"/>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="FocusStates">
|
||||||
|
<VisualState x:Name="Unfocused"/>
|
||||||
|
<VisualState x:Name="Focused">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="FocusBackground" Storyboard.TargetProperty="Fill">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemControlHighlightListLowBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<Rectangle x:Name="BorderBackground"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
Fill="Transparent"
|
||||||
|
Opacity="1"
|
||||||
|
Control.IsTemplateFocusTarget="True" />
|
||||||
|
<Rectangle x:Name="FocusBackground"
|
||||||
|
IsHitTestVisible="False"
|
||||||
|
Fill="Transparent"
|
||||||
|
Opacity="1"
|
||||||
|
Control.IsTemplateFocusTarget="True"/>
|
||||||
|
<Rectangle x:Name="SelectedPipe"
|
||||||
|
Opacity="0"
|
||||||
|
Width="4"
|
||||||
|
Height="24"
|
||||||
|
Fill="{ThemeResource SystemControlForegroundAccentBrush}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
Margin="16,0,0,0" />
|
||||||
|
</Grid>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Default style for Windows.UI.Xaml.Controls.ListBoxItem -->
|
||||||
|
<Style x:Key="ListBoxItemStyle" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="TabNavigation" Value="Local" />
|
||||||
|
<Setter Property="Padding" Value="8,10" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="LayoutRoot"
|
||||||
|
Background="{TemplateBinding Background}"
|
||||||
|
BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}">
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal" />
|
||||||
|
<VisualState x:Name="PointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemPointerOverBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemPointerOverForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="LayoutRoot"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemDisabledForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Pressed">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="PressedBackground"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
To="1"
|
||||||
|
Duration="0" />
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemPressedForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="SelectionStates">
|
||||||
|
<VisualState x:Name="Unselected" />
|
||||||
|
<VisualState x:Name="Selected">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InnerGrid"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="SelectedUnfocused">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InnerGrid"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="SelectedDisabled">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InnerGrid"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedDisabledBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedDisabledForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="SelectedPointerOver">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InnerGrid"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedPointerOverBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="SelectedPressed">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="InnerGrid"
|
||||||
|
Storyboard.TargetProperty="Background">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedBackgroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter"
|
||||||
|
Storyboard.TargetProperty="Foreground">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ListBoxItemSelectedForegroundThemeBrush}" />
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="FocusStates">
|
||||||
|
<VisualState x:Name="Focused">
|
||||||
|
<Storyboard>
|
||||||
|
<DoubleAnimation Storyboard.TargetName="FocusVisualWhite"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
To="1"
|
||||||
|
Duration="0" />
|
||||||
|
<DoubleAnimation Storyboard.TargetName="FocusVisualBlack"
|
||||||
|
Storyboard.TargetProperty="Opacity"
|
||||||
|
To="1"
|
||||||
|
Duration="0" />
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Unfocused" />
|
||||||
|
<VisualState x:Name="PointerFocused" />
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<Grid x:Name="InnerGrid"
|
||||||
|
Background="Transparent">
|
||||||
|
<Rectangle x:Name="PressedBackground"
|
||||||
|
Fill="{ThemeResource ListBoxItemPressedBackgroundThemeBrush}"
|
||||||
|
Opacity="0" />
|
||||||
|
<ContentPresenter x:Name="ContentPresenter"
|
||||||
|
Content="{TemplateBinding Content}"
|
||||||
|
ContentTransitions="{TemplateBinding ContentTransitions}"
|
||||||
|
ContentTemplate="{TemplateBinding ContentTemplate}"
|
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||||
|
Margin="{TemplateBinding Padding}" />
|
||||||
|
<Rectangle x:Name="FocusVisualWhite"
|
||||||
|
Stroke="{ThemeResource FocusVisualWhiteStrokeThemeBrush}"
|
||||||
|
StrokeEndLineCap="Square"
|
||||||
|
StrokeDashArray="1,1"
|
||||||
|
Opacity="0"
|
||||||
|
StrokeDashOffset=".5" />
|
||||||
|
<Rectangle x:Name="FocusVisualBlack"
|
||||||
|
Stroke="{ThemeResource FocusVisualBlackStrokeThemeBrush}"
|
||||||
|
StrokeEndLineCap="Square"
|
||||||
|
StrokeDashArray="1,1"
|
||||||
|
Opacity="0"
|
||||||
|
StrokeDashOffset="1.5" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="ScenarioListBoxStyle" TargetType="ListBox">
|
||||||
|
<Setter Property="Foreground" Value="{ThemeResource ListBoxForegroundThemeBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderBrush" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="{ThemeResource ListBoxBorderThemeThickness}"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled"/>
|
||||||
|
<Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="True"/>
|
||||||
|
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Enabled"/>
|
||||||
|
<Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="True"/>
|
||||||
|
<Setter Property="ScrollViewer.ZoomMode" Value="Disabled"/>
|
||||||
|
<Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False"/>
|
||||||
|
<Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="TabNavigation" Value="Once"/>
|
||||||
|
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
|
||||||
|
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
|
||||||
|
<Setter Property="ItemsPanel">
|
||||||
|
<Setter.Value>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel Background="Transparent"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBox">
|
||||||
|
<Border x:Name="LayoutRoot" BorderBrush="Transparent" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent">
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
|
<VisualState x:Name="Normal"/>
|
||||||
|
<VisualState x:Name="Disabled">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="LayoutRoot">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="FocusStates">
|
||||||
|
<VisualState x:Name="Focused">
|
||||||
|
<Storyboard>
|
||||||
|
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="LayoutRoot">
|
||||||
|
<DiscreteObjectKeyFrame KeyTime="0" Value="Transparent"/>
|
||||||
|
</ObjectAnimationUsingKeyFrames>
|
||||||
|
</Storyboard>
|
||||||
|
</VisualState>
|
||||||
|
<VisualState x:Name="Unfocused"/>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
<ScrollViewer x:Name="ScrollViewer" AutomationProperties.AccessibilityView="Raw" BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}" HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}" IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}" IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}" Padding="{TemplateBinding Padding}" TabNavigation="{TemplateBinding TabNavigation}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}" ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
|
@ -0,0 +1,509 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 48;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
0017A4902135B52800EB86D8 /* AADAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A4892135B52700EB86D8 /* AADAccountProvider.m */; };
|
||||||
|
0017A4912135B52800EB86D8 /* MSATokenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48A2135B52700EB86D8 /* MSATokenRequest.m */; };
|
||||||
|
0017A4922135B52800EB86D8 /* AADMSAAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */; };
|
||||||
|
0017A4932135B52800EB86D8 /* MSATokenCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48D2135B52700EB86D8 /* MSATokenCache.m */; };
|
||||||
|
0017A4942135B52800EB86D8 /* MSAAccountProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */; };
|
||||||
|
0017A49D2135D10E00EB86D8 /* NotificationProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 0017A49C2135D10E00EB86D8 /* NotificationProvider.m */; };
|
||||||
|
003421A921347622007FC970 /* NotificationsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 003421A821347622007FC970 /* NotificationsManager.m */; };
|
||||||
|
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823360212F114B0055F6E4 /* AppDelegate.m */; };
|
||||||
|
00823364212F114B0055F6E4 /* RootViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823363212F114B0055F6E4 /* RootViewController.m */; };
|
||||||
|
00823367212F114B0055F6E4 /* LoginViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823366212F114B0055F6E4 /* LoginViewController.m */; };
|
||||||
|
0082336A212F114B0055F6E4 /* ModelController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823369212F114B0055F6E4 /* ModelController.m */; };
|
||||||
|
0082336D212F114B0055F6E4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0082336B212F114B0055F6E4 /* Main.storyboard */; };
|
||||||
|
0082336F212F114B0055F6E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0082336E212F114B0055F6E4 /* Assets.xcassets */; };
|
||||||
|
00823372212F114B0055F6E4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00823370212F114B0055F6E4 /* LaunchScreen.storyboard */; };
|
||||||
|
00823375212F114B0055F6E4 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 00823374212F114B0055F6E4 /* main.m */; };
|
||||||
|
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D6D05F213720E5008E5E33 /* NotificationsViewController.m */; };
|
||||||
|
00D6D06321384D00008E5E33 /* HistoryViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D6D06221384D00008E5E33 /* HistoryViewController.m */; };
|
||||||
|
5D93128427C67BC7232939D5 /* libPods-GraphNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B6312F300B55978A0074729 /* libPods-GraphNotifications.a */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
0017A4892135B52700EB86D8 /* AADAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADAccountProvider.m; sourceTree = "<group>"; };
|
||||||
|
0017A48A2135B52700EB86D8 /* MSATokenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenRequest.m; sourceTree = "<group>"; };
|
||||||
|
0017A48B2135B52700EB86D8 /* MSATokenCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenCache.h; sourceTree = "<group>"; };
|
||||||
|
0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AADMSAAccountProvider.m; sourceTree = "<group>"; };
|
||||||
|
0017A48D2135B52700EB86D8 /* MSATokenCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSATokenCache.m; sourceTree = "<group>"; };
|
||||||
|
0017A48E2135B52700EB86D8 /* MSATokenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSATokenRequest.h; sourceTree = "<group>"; };
|
||||||
|
0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAAccountProvider.m; sourceTree = "<group>"; };
|
||||||
|
0017A4952135B53C00EB86D8 /* SingleUserAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SingleUserAccountProvider.h; sourceTree = "<group>"; };
|
||||||
|
0017A4962135B53C00EB86D8 /* MSAAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSAAccountProvider.h; sourceTree = "<group>"; };
|
||||||
|
0017A4972135B53C00EB86D8 /* SampleAccountActionFailureReason.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SampleAccountActionFailureReason.h; sourceTree = "<group>"; };
|
||||||
|
0017A4982135B53C00EB86D8 /* AADAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADAccountProvider.h; sourceTree = "<group>"; };
|
||||||
|
0017A4992135B53C00EB86D8 /* AADMSAAccountProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AADMSAAccountProvider.h; sourceTree = "<group>"; };
|
||||||
|
0017A49B2135D10A00EB86D8 /* NotificationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationProvider.h; sourceTree = "<group>"; };
|
||||||
|
0017A49C2135D10E00EB86D8 /* NotificationProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationProvider.m; sourceTree = "<group>"; };
|
||||||
|
003421A72130A887007FC970 /* NotificationsManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationsManager.h; sourceTree = "<group>"; };
|
||||||
|
003421A821347622007FC970 /* NotificationsManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationsManager.m; sourceTree = "<group>"; };
|
||||||
|
0082335C212F114B0055F6E4 /* GraphNotifications.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GraphNotifications.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
0082335F212F114B0055F6E4 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
|
||||||
|
00823360212F114B0055F6E4 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
|
||||||
|
00823362212F114B0055F6E4 /* RootViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RootViewController.h; sourceTree = "<group>"; };
|
||||||
|
00823363212F114B0055F6E4 /* RootViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RootViewController.m; sourceTree = "<group>"; };
|
||||||
|
00823365212F114B0055F6E4 /* LoginViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoginViewController.h; sourceTree = "<group>"; };
|
||||||
|
00823366212F114B0055F6E4 /* LoginViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoginViewController.m; sourceTree = "<group>"; };
|
||||||
|
00823368212F114B0055F6E4 /* ModelController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModelController.h; sourceTree = "<group>"; };
|
||||||
|
00823369212F114B0055F6E4 /* ModelController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModelController.m; sourceTree = "<group>"; };
|
||||||
|
0082336C212F114B0055F6E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
0082336E212F114B0055F6E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
00823371212F114B0055F6E4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
00823373212F114B0055F6E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
00823374212F114B0055F6E4 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||||
|
00D6D05E21372041008E5E33 /* NotificationsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationsViewController.h; sourceTree = "<group>"; };
|
||||||
|
00D6D05F213720E5008E5E33 /* NotificationsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotificationsViewController.m; sourceTree = "<group>"; };
|
||||||
|
00D6D06121384BA3008E5E33 /* HistoryViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HistoryViewController.h; sourceTree = "<group>"; };
|
||||||
|
00D6D06221384D00008E5E33 /* HistoryViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HistoryViewController.m; sourceTree = "<group>"; };
|
||||||
|
00D6D0642138519C008E5E33 /* GraphNotificationsSample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GraphNotificationsSample.entitlements; sourceTree = "<group>"; };
|
||||||
|
63E3FBAFA254E6B80A8DA76E /* Pods-GraphNotifications.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphNotifications.release.xcconfig"; path = "Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
9B6312F300B55978A0074729 /* libPods-GraphNotifications.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-GraphNotifications.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
FFB1B176FF8985E5AD4B88D1 /* Pods-GraphNotifications.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GraphNotifications.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
00823359212F114B0055F6E4 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
5D93128427C67BC7232939D5 /* libPods-GraphNotifications.a in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
0017A49A2135B55400EB86D8 /* SampleAccountProviders */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0017A4982135B53C00EB86D8 /* AADAccountProvider.h */,
|
||||||
|
0017A4992135B53C00EB86D8 /* AADMSAAccountProvider.h */,
|
||||||
|
0017A4962135B53C00EB86D8 /* MSAAccountProvider.h */,
|
||||||
|
0017A4972135B53C00EB86D8 /* SampleAccountActionFailureReason.h */,
|
||||||
|
0017A4952135B53C00EB86D8 /* SingleUserAccountProvider.h */,
|
||||||
|
0017A4892135B52700EB86D8 /* AADAccountProvider.m */,
|
||||||
|
0017A48C2135B52700EB86D8 /* AADMSAAccountProvider.m */,
|
||||||
|
0017A48F2135B52800EB86D8 /* MSAAccountProvider.m */,
|
||||||
|
0017A48B2135B52700EB86D8 /* MSATokenCache.h */,
|
||||||
|
0017A48D2135B52700EB86D8 /* MSATokenCache.m */,
|
||||||
|
0017A48E2135B52700EB86D8 /* MSATokenRequest.h */,
|
||||||
|
0017A48A2135B52700EB86D8 /* MSATokenRequest.m */,
|
||||||
|
);
|
||||||
|
path = SampleAccountProviders;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
00823353212F114A0055F6E4 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0017A49A2135B55400EB86D8 /* SampleAccountProviders */,
|
||||||
|
0082335E212F114B0055F6E4 /* GraphNotificationsSample */,
|
||||||
|
0082335D212F114B0055F6E4 /* Products */,
|
||||||
|
D9CF36B91E745B7CB13F10B3 /* Pods */,
|
||||||
|
31E53F56A7E5C56EF7B399C4 /* Frameworks */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
0082335D212F114B0055F6E4 /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0082335C212F114B0055F6E4 /* GraphNotifications.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
0082335E212F114B0055F6E4 /* GraphNotificationsSample */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
00D6D0642138519C008E5E33 /* GraphNotificationsSample.entitlements */,
|
||||||
|
0017A49B2135D10A00EB86D8 /* NotificationProvider.h */,
|
||||||
|
0017A49C2135D10E00EB86D8 /* NotificationProvider.m */,
|
||||||
|
0082335F212F114B0055F6E4 /* AppDelegate.h */,
|
||||||
|
00823360212F114B0055F6E4 /* AppDelegate.m */,
|
||||||
|
00823362212F114B0055F6E4 /* RootViewController.h */,
|
||||||
|
00823363212F114B0055F6E4 /* RootViewController.m */,
|
||||||
|
00823365212F114B0055F6E4 /* LoginViewController.h */,
|
||||||
|
00823366212F114B0055F6E4 /* LoginViewController.m */,
|
||||||
|
00823368212F114B0055F6E4 /* ModelController.h */,
|
||||||
|
00823369212F114B0055F6E4 /* ModelController.m */,
|
||||||
|
0082336B212F114B0055F6E4 /* Main.storyboard */,
|
||||||
|
0082336E212F114B0055F6E4 /* Assets.xcassets */,
|
||||||
|
00823370212F114B0055F6E4 /* LaunchScreen.storyboard */,
|
||||||
|
00823373212F114B0055F6E4 /* Info.plist */,
|
||||||
|
00823374212F114B0055F6E4 /* main.m */,
|
||||||
|
003421A72130A887007FC970 /* NotificationsManager.h */,
|
||||||
|
003421A821347622007FC970 /* NotificationsManager.m */,
|
||||||
|
00D6D05E21372041008E5E33 /* NotificationsViewController.h */,
|
||||||
|
00D6D05F213720E5008E5E33 /* NotificationsViewController.m */,
|
||||||
|
00D6D06121384BA3008E5E33 /* HistoryViewController.h */,
|
||||||
|
00D6D06221384D00008E5E33 /* HistoryViewController.m */,
|
||||||
|
);
|
||||||
|
path = GraphNotificationsSample;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
31E53F56A7E5C56EF7B399C4 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9B6312F300B55978A0074729 /* libPods-GraphNotifications.a */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
D9CF36B91E745B7CB13F10B3 /* Pods */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FFB1B176FF8985E5AD4B88D1 /* Pods-GraphNotifications.debug.xcconfig */,
|
||||||
|
63E3FBAFA254E6B80A8DA76E /* Pods-GraphNotifications.release.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Pods;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
0082335B212F114B0055F6E4 /* GraphNotificationsSample */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 00823378212F114B0055F6E4 /* Build configuration list for PBXNativeTarget "GraphNotificationsSample" */;
|
||||||
|
buildPhases = (
|
||||||
|
BBBC242D2C377CDA52234B8A /* [CP] Check Pods Manifest.lock */,
|
||||||
|
00823358212F114B0055F6E4 /* Sources */,
|
||||||
|
00823359212F114B0055F6E4 /* Frameworks */,
|
||||||
|
0082335A212F114B0055F6E4 /* Resources */,
|
||||||
|
534E2B6519087712F45067D1 /* [CP] Embed Pods Frameworks */,
|
||||||
|
DF877AD63B2A176428A85DFC /* [CP] Copy Pods Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = GraphNotificationsSample;
|
||||||
|
productName = GraphNotificationsSample;
|
||||||
|
productReference = 0082335C212F114B0055F6E4 /* GraphNotifications.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
00823354212F114A0055F6E4 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
LastUpgradeCheck = 0920;
|
||||||
|
ORGANIZATIONNAME = Microsoft;
|
||||||
|
TargetAttributes = {
|
||||||
|
0082335B212F114B0055F6E4 = {
|
||||||
|
CreatedOnToolsVersion = 9.2;
|
||||||
|
ProvisioningStyle = Manual;
|
||||||
|
SystemCapabilities = {
|
||||||
|
com.apple.Push = {
|
||||||
|
enabled = 1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 00823357212F114A0055F6E4 /* Build configuration list for PBXProject "GraphNotificationsSample" */;
|
||||||
|
compatibilityVersion = "Xcode 8.0";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 00823353212F114A0055F6E4;
|
||||||
|
productRefGroup = 0082335D212F114B0055F6E4 /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
0082335B212F114B0055F6E4 /* GraphNotificationsSample */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
0082335A212F114B0055F6E4 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
00823372212F114B0055F6E4 /* LaunchScreen.storyboard in Resources */,
|
||||||
|
0082336F212F114B0055F6E4 /* Assets.xcassets in Resources */,
|
||||||
|
0082336D212F114B0055F6E4 /* Main.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
534E2B6519087712F45067D1 /* [CP] Embed Pods Frameworks */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[CP] Embed Pods Frameworks";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications-frameworks.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
BBBC242D2C377CDA52234B8A /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
DF877AD63B2A176428A85DFC /* [CP] Copy Pods Resources */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "[CP] Copy Pods Resources";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-GraphNotifications/Pods-GraphNotifications-resources.sh\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
00823358212F114B0055F6E4 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
0017A4922135B52800EB86D8 /* AADMSAAccountProvider.m in Sources */,
|
||||||
|
00823375212F114B0055F6E4 /* main.m in Sources */,
|
||||||
|
00823364212F114B0055F6E4 /* RootViewController.m in Sources */,
|
||||||
|
003421A921347622007FC970 /* NotificationsManager.m in Sources */,
|
||||||
|
0017A4942135B52800EB86D8 /* MSAAccountProvider.m in Sources */,
|
||||||
|
00D6D06321384D00008E5E33 /* HistoryViewController.m in Sources */,
|
||||||
|
0017A4932135B52800EB86D8 /* MSATokenCache.m in Sources */,
|
||||||
|
0082336A212F114B0055F6E4 /* ModelController.m in Sources */,
|
||||||
|
00D6D060213720E5008E5E33 /* NotificationsViewController.m in Sources */,
|
||||||
|
0017A49D2135D10E00EB86D8 /* NotificationProvider.m in Sources */,
|
||||||
|
0017A4902135B52800EB86D8 /* AADAccountProvider.m in Sources */,
|
||||||
|
00823367212F114B0055F6E4 /* LoginViewController.m in Sources */,
|
||||||
|
00823361212F114B0055F6E4 /* AppDelegate.m in Sources */,
|
||||||
|
0017A4912135B52800EB86D8 /* MSATokenRequest.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
0082336B212F114B0055F6E4 /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
0082336C212F114B0055F6E4 /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
00823370212F114B0055F6E4 /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
00823371212F114B0055F6E4 /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
00823376212F114B0055F6E4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.2;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
00823377212F114B0055F6E4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.2;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
00823379212F114B0055F6E4 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = FFB1B176FF8985E5AD4B88D1 /* Pods-GraphNotifications.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = GraphNotificationsSample/GraphNotificationsSample.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = UBF8T346G9;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = GraphNotificationsSample/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
LIBRARY_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/GraphNotificationsSample/ConnectedDevices",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE = "ba3afbc5-30e6-4150-9eb9-d8967de4ce9b";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "Graph Notifications Development";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
0082337A212F114B0055F6E4 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 63E3FBAFA254E6B80A8DA76E /* Pods-GraphNotifications.release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = GraphNotificationsSample/GraphNotificationsSample.entitlements;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = UBF8T346G9;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = GraphNotificationsSample/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
|
LIBRARY_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"$(PROJECT_DIR)/GraphNotificationsSample/ConnectedDevices",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.microsoft.connecteddevices.graphnotifications;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE = "ba3afbc5-30e6-4150-9eb9-d8967de4ce9b";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "Graph Notifications Development";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
00823357212F114A0055F6E4 /* Build configuration list for PBXProject "GraphNotificationsSample" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
00823376212F114B0055F6E4 /* Debug */,
|
||||||
|
00823377212F114B0055F6E4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
00823378212F114B0055F6E4 /* Build configuration list for PBXNativeTarget "GraphNotificationsSample" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
00823379212F114B0055F6E4 /* Debug */,
|
||||||
|
0082337A212F114B0055F6E4 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 00823354212F114A0055F6E4 /* Project object */;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
//
|
||||||
|
// AppDelegate.h
|
||||||
|
// GraphNotifications
|
||||||
|
//
|
||||||
|
// Created by Allen Ballway on 8/23/18.
|
||||||
|
// Copyright © 2018 Microsoft. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||||
|
|
||||||
|
@property (strong, nonatomic) UIWindow *window;
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
#import "NotificationProvider.h"
|
||||||
|
|
||||||
|
|
||||||
|
void uncaughtExceptionHandler(NSException* uncaughtException)
|
||||||
|
{
|
||||||
|
NSLog(@"Uncaught exception: %@", uncaughtException.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface AppDelegate ()
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
|
||||||
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||||
|
NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
|
||||||
|
|
||||||
|
// Set up Notifications
|
||||||
|
NSDictionary* userInfo = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
|
||||||
|
|
||||||
|
if (!userInfo)
|
||||||
|
{
|
||||||
|
// User launch app by tapping the App icon normal launch
|
||||||
|
[application
|
||||||
|
registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert categories:nil]];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
// app run in background and received the push notification, app is launched by user tapping the alert view
|
||||||
|
[MCDNotificationReceiver receiveNotification:userInfo];
|
||||||
|
}
|
||||||
|
@catch(NSException* exception) {
|
||||||
|
NSLog(@"Failed start up notification with exception %@", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)applicationWillResignActive:(UIApplication *)application {
|
||||||
|
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||||
|
// Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||||
|
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||||
|
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)applicationWillEnterForeground:(UIApplication *)application {
|
||||||
|
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)applicationDidBecomeActive:(UIApplication *)application {
|
||||||
|
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (void)applicationWillTerminate:(UIApplication *)application {
|
||||||
|
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)application:(UIApplication*)application
|
||||||
|
didRegisterUserNotificationSettings:(__unused UIUserNotificationSettings*)notificationSettings
|
||||||
|
{
|
||||||
|
// actually registerForRemoteNotifications after registerUserNotificationSettings is finished
|
||||||
|
[application registerForRemoteNotifications];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)application:(__unused UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
|
||||||
|
{
|
||||||
|
// when registerForRemoteNotifications, retrieve the deviceToken, convert it to HEX encoded NSString
|
||||||
|
NSMutableString* deviceTokenStr = [NSMutableString stringWithCapacity:deviceToken.length * 2];
|
||||||
|
const unsigned char* byteBuffer = deviceToken.bytes;
|
||||||
|
for (NSUInteger i = 0; i < deviceToken.length; ++i)
|
||||||
|
{
|
||||||
|
[deviceTokenStr appendFormat:@"%02X", (unsigned int)byteBuffer[i]];
|
||||||
|
}
|
||||||
|
NSLog(@"APNs token: %@", deviceTokenStr);
|
||||||
|
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
// invoke notificationProvider with new notification registration
|
||||||
|
[NotificationProvider
|
||||||
|
updateNotificationRegistration:[[MCDNotificationRegistration alloc]
|
||||||
|
initWithNotificationType:MCDNotificationTypeAPN
|
||||||
|
token:deviceTokenStr
|
||||||
|
appId:[[NSBundle mainBundle] bundleIdentifier]
|
||||||
|
appDisplayName:(NSString*)[[NSBundle mainBundle]
|
||||||
|
objectForInfoDictionaryKey:@"CFBundleDisplayName"]]];
|
||||||
|
}
|
||||||
|
@catch (NSException* exception) {
|
||||||
|
NSLog(@"Failed to update notification registration with exception %@", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)application:(__unused UIApplication*)application didReceiveRemoteNotification:(nonnull NSDictionary*)userInfo
|
||||||
|
{
|
||||||
|
// app run in foreground and received the push notification, pump notification into CDPPlatform
|
||||||
|
NSLog(@"Received remote notification...");
|
||||||
|
[userInfo enumerateKeysAndObjectsUsingBlock:^(
|
||||||
|
id _Nonnull key, id _Nonnull obj, __unused BOOL* _Nonnull stop) { NSLog(@"%@: %@", key, obj); }];
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
if (![MCDNotificationReceiver receiveNotification:userInfo])
|
||||||
|
{
|
||||||
|
NSLog(@"Received notification was not for Rome");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@catch(NSException* exception) {
|
||||||
|
NSLog(@"Failed to receive notification with exception %@", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,98 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "20x20",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "20x20",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "60x60",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"size" : "60x60",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "20x20",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "20x20",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "29x29",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "40x40",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "76x76",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "76x76",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="703-4V-yAM">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Root View Controller-->
|
||||||
|
<scene sceneID="clB-vc-fyl">
|
||||||
|
<objects>
|
||||||
|
<viewController id="703-4V-yAM" customClass="RootViewController" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="502-Ir-ELC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" red="0.60000002379999995" green="0.40000000600000002" blue="0.20000000300000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="iAZ-4m-2z3"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Q3h-pU-vEd" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-101" y="-243"/>
|
||||||
|
</scene>
|
||||||
|
<!--Login-->
|
||||||
|
<scene sceneID="snT-py-3hH">
|
||||||
|
<objects>
|
||||||
|
<viewController storyboardIdentifier="LoginViewController" title="Login" id="S4R-Ja-viH" customClass="LoginViewController" sceneMemberID="viewController">
|
||||||
|
<view key="view" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="ZwX-cT-FIQ">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="s1b-V9-EN7">
|
||||||
|
<rect key="frame" x="20" y="20" width="339" height="627"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="fz1-y2-7rY">
|
||||||
|
<rect key="frame" x="0.0" y="112" width="335" height="30"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<state key="normal" title="Login Work/School Account"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="loginAAD" destination="S4R-Ja-viH" eventType="touchUpInside" id="jIC-Q3-hja"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nW6-tV-2PU">
|
||||||
|
<rect key="frame" x="0.0" y="289" width="335" height="30"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
|
<state key="normal" title="Login Personal Account"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="loginMSA" destination="S4R-Ja-viH" eventType="touchUpInside" id="JLQ-Q2-VCr"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="0.97826086960000003" green="0.91848131079999995" blue="0.73914263440000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="K9I-jD-KjA"/>
|
||||||
|
</view>
|
||||||
|
<connections>
|
||||||
|
<outlet property="aadButton" destination="fz1-y2-7rY" id="W6O-WH-oAW"/>
|
||||||
|
<outlet property="msaButton" destination="nW6-tV-2PU" id="RsH-8N-pIc"/>
|
||||||
|
</connections>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="GmD-Rr-ZGN" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="596" y="-243.32833583208398"/>
|
||||||
|
</scene>
|
||||||
|
<!--Notifications View Controller-->
|
||||||
|
<scene sceneID="BEH-RM-JJY">
|
||||||
|
<objects>
|
||||||
|
<tableViewController storyboardIdentifier="NotificationsViewController" id="7uX-h6-yAZ" customClass="NotificationsViewController" sceneMemberID="viewController">
|
||||||
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="bd2-mg-Jex">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<prototypes>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="5s3-lz-tU0">
|
||||||
|
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5s3-lz-tU0" id="UNp-yx-8lI">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
</tableViewCell>
|
||||||
|
</prototypes>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="7uX-h6-yAZ" id="hEu-dZ-9bH"/>
|
||||||
|
<outlet property="delegate" destination="7uX-h6-yAZ" id="Jfe-fL-s75"/>
|
||||||
|
</connections>
|
||||||
|
</tableView>
|
||||||
|
</tableViewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="gyR-70-0hf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="1310" y="161"/>
|
||||||
|
</scene>
|
||||||
|
<!--Table View Controller-->
|
||||||
|
<scene sceneID="POB-cl-eRc">
|
||||||
|
<objects>
|
||||||
|
<tableViewController id="TH6-n8-5Lo" sceneMemberID="viewController">
|
||||||
|
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="rcJ-uf-0Gl">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<prototypes>
|
||||||
|
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" id="FQB-xj-D8j">
|
||||||
|
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="FQB-xj-D8j" id="IIp-5u-wBC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</tableViewCellContentView>
|
||||||
|
</tableViewCell>
|
||||||
|
</prototypes>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="TH6-n8-5Lo" id="97g-p9-rkt"/>
|
||||||
|
<outlet property="delegate" destination="TH6-n8-5Lo" id="3lj-z6-Hv2"/>
|
||||||
|
</connections>
|
||||||
|
</tableView>
|
||||||
|
</tableViewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="1pT-t6-eTV" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="494" y="483"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,8 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import "NotificationsManager.h"
|
||||||
|
|
||||||
|
@interface HistoryViewController : UITableViewController
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "HistoryViewController.h"
|
||||||
|
#import "AdaptiveCards/ACFramework.h"
|
||||||
|
|
||||||
|
|
||||||
|
@interface HistoryViewController() {
|
||||||
|
}
|
||||||
|
@property (nonatomic) NSMutableArray<MCDUserNotification*>* notifications;
|
||||||
|
- (void)updateData;
|
||||||
|
- (void)handleTap:(UITapGestureRecognizer*)recognizer;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation HistoryViewController
|
||||||
|
|
||||||
|
- (void)viewDidLoad {
|
||||||
|
[super viewDidLoad];
|
||||||
|
// Do any additional setup after loading the view, typically from a nib.
|
||||||
|
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
[[NotificationsManager sharedInstance] addNotificationsChangedListener:^{
|
||||||
|
[self updateData];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
[weakSelf.tableView reloadData];
|
||||||
|
});
|
||||||
|
}];
|
||||||
|
|
||||||
|
[self updateData];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)numberOfSectionsInTableView:(__unused UITableView*)tableView
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (nonnull UITableViewCell*)tableView:(nonnull UITableView*)tableView cellForRowAtIndexPath:(nonnull __unused NSIndexPath*)indexPath
|
||||||
|
{
|
||||||
|
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"NotificationCell"];
|
||||||
|
MCDUserNotification* notification = self.notifications[indexPath.row];
|
||||||
|
|
||||||
|
ACOAdaptiveCardParseResult* parseResult = [ACOAdaptiveCard fromJson:notification.content];
|
||||||
|
ACRRenderResult* renderResult = [ACRRenderer render:parseResult.card config:[ACOHostConfig new] widthConstraint:0.0f];
|
||||||
|
|
||||||
|
[cell.contentView addSubview:renderResult.view];
|
||||||
|
[cell addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)]];
|
||||||
|
return cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSInteger)tableView:(nonnull __unused UITableView*)tableView numberOfRowsInSection:(__unused NSInteger)section
|
||||||
|
{
|
||||||
|
return self.notifications.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleTap:(UITapGestureRecognizer *)recognizer {
|
||||||
|
[[NotificationsManager sharedInstance] readNotificationAtIndex:[self.tableView indexPathForCell:(UITableViewCell*)recognizer.view].row];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)updateData {
|
||||||
|
@synchronized (self) {
|
||||||
|
[self.notifications removeAllObjects];
|
||||||
|
for (MCDUserNotification* notification in [NotificationsManager sharedInstance].notifications) {
|
||||||
|
if (notification.status == MCDUserNotificationStatusActive && notification.readState == MCDUserNotificationReadStateUnread && notification.userActionState != MCDUserNotificationUserActionStateNoInteraction) {
|
||||||
|
[self.notifications addObject:notification];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>armv7</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@interface LoginViewController : UIViewController
|
||||||
|
- (IBAction)loginAAD;
|
||||||
|
- (IBAction)loginMSA;
|
||||||
|
@property (strong, nonatomic) IBOutlet UIButton *aadButton;
|
||||||
|
@property (strong, nonatomic) IBOutlet UIButton *msaButton;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
// DataViewController.m
|
||||||
|
// GraphNotifications
|
||||||
|
//
|
||||||
|
// Created by Allen Ballway on 8/23/18.
|
||||||
|
// Copyright © 2018 Microsoft. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "LoginViewController.h"
|
||||||
|
#import "NotificationsManager.h"
|
||||||
|
|
||||||
|
@interface LoginViewController ()
|
||||||
|
@property (nonatomic) AADMSAAccountProvider* accountProvider;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation LoginViewController
|
||||||
|
|
||||||
|
- (void)viewDidLoad {
|
||||||
|
[super viewDidLoad];
|
||||||
|
// Do any additional setup after loading the view, typically from a nib.
|
||||||
|
|
||||||
|
self.accountProvider = [NotificationsManager sharedInstance].accountProvider;
|
||||||
|
[self setButtonTextForState:self.accountProvider.signInState];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)loginAAD {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
switch (self.accountProvider.signInState) {
|
||||||
|
case AADMSAAccountProviderSignInStateSignedInAAD: {
|
||||||
|
[self.accountProvider signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||||
|
if (successful) {
|
||||||
|
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed to sign out AAD with reason %ld", reason);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AADMSAAccountProviderSignInStateSignedOut: {
|
||||||
|
[self.accountProvider signInAADWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||||
|
if (successful) {
|
||||||
|
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed to sign in with AAD with reason %ld", reason);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)loginMSA {
|
||||||
|
__weak typeof(self) weakSelf = self;
|
||||||
|
switch (self.accountProvider.signInState) {
|
||||||
|
case AADMSAAccountProviderSignInStateSignedInMSA: {
|
||||||
|
[self.accountProvider signOutWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||||
|
if (successful) {
|
||||||
|
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed to sign out MSA with reason %ld", reason);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AADMSAAccountProviderSignInStateSignedOut: {
|
||||||
|
[self.accountProvider signInMSAWithCompletionCallback:^(BOOL successful, SampleAccountActionFailureReason reason) {
|
||||||
|
if (successful) {
|
||||||
|
[weakSelf setButtonTextForState:weakSelf.accountProvider.signInState];
|
||||||
|
} else {
|
||||||
|
NSLog(@"Failed to sign in with MSA with reason %ld", reason);
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setButtonTextForState:(AADMSAAccountProviderSignInState)state {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
switch (state) {
|
||||||
|
case AADMSAAccountProviderSignInStateSignedOut:
|
||||||
|
[self.aadButton setTitle:@"Login Work/School Account" forState:UIControlStateNormal];
|
||||||
|
[self.msaButton setTitle:@"Login Personal Account" forState:UIControlStateNormal];
|
||||||
|
break;
|
||||||
|
case AADMSAAccountProviderSignInStateSignedInAAD:
|
||||||
|
[self.aadButton setTitle:@"Logout" forState:UIControlStateNormal];
|
||||||
|
[self.msaButton setTitle:@"" forState:UIControlStateNormal];
|
||||||
|
break;
|
||||||
|
case AADMSAAccountProviderSignInStateSignedInMSA:
|
||||||
|
[self.aadButton setTitle:@"" forState:UIControlStateNormal];
|
||||||
|
[self.msaButton setTitle:@"Logout" forState:UIControlStateNormal];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@end
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// ModelController.h
|
||||||
|
// GraphNotifications
|
||||||
|
//
|
||||||
|
// Created by Allen Ballway on 8/23/18.
|
||||||
|
// Copyright © 2018 Microsoft. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@class LoginViewController;
|
||||||
|
|
||||||
|
@interface ModelController : NSObject <UIPageViewControllerDataSource>
|
||||||
|
|
||||||
|
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard;
|
||||||
|
- (NSUInteger)indexOfViewController:(UIViewController *)viewController;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
#import "ModelController.h"
|
||||||
|
#import "LoginViewController.h"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@interface ModelController ()
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation ModelController
|
||||||
|
|
||||||
|
- (instancetype)init {
|
||||||
|
self = [super init];
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIViewController *)viewControllerAtIndex:(NSUInteger)index storyboard:(UIStoryboard *)storyboard {
|
||||||
|
|
||||||
|
switch(index) {
|
||||||
|
case 0: {
|
||||||
|
LoginViewController *dataViewController = [storyboard instantiateViewControllerWithIdentifier:@"LoginViewController"];
|
||||||
|
return dataViewController;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (NSUInteger)indexOfViewController:(UIViewController *)viewController {
|
||||||
|
if ([viewController isKindOfClass:[LoginViewController class]]) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Page View Controller Data Source
|
||||||
|
|
||||||
|
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
|
||||||
|
{
|
||||||
|
NSUInteger index = [self indexOfViewController:viewController];
|
||||||
|
if ((index == 0) || (index == NSNotFound)) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [self viewControllerAtIndex:--index storyboard:viewController.storyboard];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
|
||||||
|
{
|
||||||
|
NSUInteger index = [self indexOfViewController:viewController];
|
||||||
|
if (index == NSNotFound || index >= 2) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [self viewControllerAtIndex:++index storyboard:viewController.storyboard];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,19 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
#import <ConnectedDevices/Core/Core.h>
|
||||||
|
|
||||||
|
// @brief provides an sample implementation of MCDNotificationProvider
|
||||||
|
@interface NotificationProvider : NSObject <MCDNotificationProvider>
|
||||||
|
|
||||||
|
// @brief get the shared instance of MCDNotificationProvider
|
||||||
|
+ (nullable instancetype)sharedInstance;
|
||||||
|
|
||||||
|
// @brief class method update the notification registration provider with new notification registration
|
||||||
|
+ (void)updateNotificationRegistration:(nonnull MCDNotificationRegistration*)notificationRegistration;
|
||||||
|
|
||||||
|
@end
|
|
@ -0,0 +1,67 @@
|
||||||
|
//
|
||||||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import "NotificationProvider.h"
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
@implementation NotificationProvider
|
||||||
|
{
|
||||||
|
MCDRegistrationUpdatedEvent* _registrationUpdated;
|
||||||
|
MCDNotificationRegistration* _notificationRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)sharedInstance
|
||||||
|
{
|
||||||
|
static NotificationProvider* sharedInstance = nil;
|
||||||
|
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
dispatch_once(&onceToken, ^{ sharedInstance = [[NotificationProvider alloc] init]; });
|
||||||
|
|
||||||
|
return sharedInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
|
||||||
|
{
|
||||||
|
NSLog(@"Raise notification registration changed event\nType: %ld\nApplication: %@", (long)notificationRegistration.type,
|
||||||
|
notificationRegistration.applicationId);
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
|
||||||
|
^{ [[NotificationProvider sharedInstance] _updateNotificationRegistration:notificationRegistration]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCDNotificationProvider
|
||||||
|
@synthesize registrationUpdated = _registrationUpdated;
|
||||||
|
|
||||||
|
- (void)getNotificationRegistrationAsync:(nonnull void (^)(MCDNotificationRegistration* _Nullable, NSError* _Nullable))completionBlock
|
||||||
|
{
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ completionBlock(_notificationRegistration, nil); });
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)init
|
||||||
|
{
|
||||||
|
if (self = [super init])
|
||||||
|
{
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
_registrationUpdated = [MCDRegistrationUpdatedEvent new];
|
||||||
|
}
|
||||||
|
@catch(NSException* e) {
|
||||||
|
NSLog(@"Failed to create new MCDRegistrationUpdatedEvent with exception %@", e.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_updateNotificationRegistration:(MCDNotificationRegistration*)notificationRegistration
|
||||||
|
{
|
||||||
|
_notificationRegistration = notificationRegistration;
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
[_registrationUpdated raiseWithNotificationRegistration:notificationRegistration];
|
||||||
|
}
|
||||||
|
@catch(NSException* e) {
|
||||||
|
NSLog(@"Failed to update notification registration with exception %@", e.description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@end
|