Merged PR 11519: GraphNotifications SDK Samples

GraphNotifications/UserNotifications Samples for the following Platforms
- Android
- iOS
- Windows
This commit is contained in:
Sudipta Dey (WDG) 2018-09-22 02:42:52 +00:00 коммит произвёл Brian Bowman
Родитель eed9f29dde
Коммит de0dab5024
120 изменённых файлов: 9593 добавлений и 0 удалений

9
Android/samples/graphnotificationssample/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild

1
Android/samples/graphnotificationssample/app/.gitignore поставляемый Normal file
Просмотреть файл

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

21
Android/samples/graphnotificationssample/app/proguard-rules.pro поставляемый Normal file
Просмотреть файл

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

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.9 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 2.8 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.9 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.3 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 10 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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

Двоичный файл не отображается.

Просмотреть файл

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

160
Android/samples/graphnotificationssample/gradlew поставляемый Normal file
Просмотреть файл

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

90
Android/samples/graphnotificationssample/gradlew.bat поставляемый Normal file
Просмотреть файл

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

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/microsoft-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 8.8 KiB

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/placeholder.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.5 KiB

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/smalltile-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 670 B

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/splash-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.1 KiB

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/squaretile-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/storelogo-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 442 B

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/tile-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 4.7 KiB

Двоичные данные
Windows/samples/GraphNotificationsSample/Assets/windows-sdk.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 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)' &lt; '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="&#xA9; 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="&#xE700;" 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="&#xE700;" />
<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

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше