Microsoft Graph を使って Android アプリを構築する
このチュートリアルでは、Microsoft Graph API を使用してユーザーの予定表情報を取得する Android アプリを構築する方法について説明します。
ヒント
完了したチュートリアルをダウンロードする場合は、リポジトリをダウンロードまたは複製GitHubできます。
前提条件
このチュートリアルを開始する前に、開発マシンに Android Studio がインストールされている必要があります。
また、Outlook.com 上のメールボックスを持つ個人用 Microsoft アカウント、または Microsoft の仕事用または学校用のアカウントを持っている必要があります。 Microsoft アカウントをお持ちでない場合は、無料アカウントを取得するためのオプションが 2 つご利用できます。
- 新しい 個人用 Microsoft アカウントにサインアップできます。
- 開発者プログラムにサインアップして、Microsoft 365サブスクリプションをMicrosoft 365できます。
注意
このチュートリアルは、Android Studio バージョン 4.1.3 と Android 10.0 SDK で記述されています。 このガイドの手順は、他のバージョンでも動作しますが、テストされていない場合があります。
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
Android アプリの作成
まず、新しい Android Studio プロジェクトを作成します。
Android Studio を開き、ようこそ 画面で [新しい Android Studio プロジェクト を開始する] を選択します。
[新しい データベースの作成] ダイアログProject[空のアクティビティ] を選択し、[次へ] を 選択します。
[プロジェクト の構成] ダイアログで、[名前
Graph Tutorial
Java
] に設定し、[言語] フィールドを [言語] フィールドに設定し、[最小 API レベル] をに設定しますAPI 29: Android 10.0 (Q)
。 必要に 応じて、[パッケージ名] と [保存場所 ] を変更します。 [完了] を選択します。
重要
このチュートリアルのコードと手順では、 パッケージ名 com.example.graphtutorial を使用します。 プロジェクトの作成時に別のパッケージ名を使用する場合は、この値が表示されている場所でパッケージ名を使用してください。
依存関係のインストール
次に進む前に、後で使用する追加の依存関係をインストールします。
com.google.android.material:material
をクリックして 、ナビゲーション ビュー をアプリで使用できます。- Android 用の Microsoft 認証ライブラリ (MSAL) は、認証Azure ADトークン管理を処理します。
- Microsoft Graph呼び出Javaを行う場合に使用する SDK Graph。
[ Gradle Scripts] を展開 し、 build.gradle (Module: Graph_Tutorial.app) を開きます。
値の内部に次の行を追加
dependencies
します。implementation 'com.google.android.material:material:1.3.0' implementation 'com.microsoft.identity.client:msal:2.0.8' implementation ('com.microsoft.graph:microsoft-graph:3.1.0') { exclude group: 'javax.activation' }
packagingOptions
android
build.gradle (Module: Graph_Tutorial.app) の値の中に値を追加します。packagingOptions { pickFirst 'META-INF/*' }
MSAL の依存関係である MicrosoftDeviceSDK ライブラリの Azure Maven リポジトリを追加します。 build.gradle (Project: Graph_Tutorial) を開きます。 値の内側の値に次
repositories
の値を追加allprojects
します。maven { url 'https://pkgs.dev.azure.com/MicrosoftDeviceSDK/DuoSDK-Public/_packaging/Duo-SDK-Feed/maven/v1' }
変更内容を保存します。 [ファイル] メニューの [Gradle ファイルProject同期] を選択します。
アプリを設計する
アプリケーションは、ナビゲーション ドロワーを使用して、さまざまなビュー間を移動します。 この手順では、ナビゲーション ドロワー レイアウトを使用するアクティビティを更新し、ビューのフラグメントを追加します。
ナビゲーション ドロワーの作成
このセクションでは、アプリのナビゲーション メニューのアイコンを作成し、アプリケーションのメニューを作成し、ナビゲーション ドロワーと互換性があるアプリケーションのテーマとレイアウトを更新します。
アイコンの作成
アプリ/ res/drawable フォルダー を右クリックし、[新規]、次に [ベクター アセット] の順に選択します。
[クリップ アート] の横にあるアイコン ボタン をクリックします。
[アイコン の選択] ウィンドウ で、検索
home
バーに入力し、[ホーム] アイコン を選択 して [OK] を選択 します。[名前] をに変更 します
ic_menu_home
。[次 へ] を 選択し、[完了 ] を選択します。
前の手順を繰り返して、さらに 4 つのアイコンを作成します。
- 名前:
ic_menu_calendar
、アイコン:event
- 名前:
ic_menu_add_event
、アイコン:add box
- 名前:
ic_menu_signout
、アイコン:exit to app
- 名前:
ic_menu_signin
、アイコン:person add
- 名前:
メニューの作成
res フォルダーを 右クリックし、[ 新規] を **選択し、[**Android リソース ディレクトリ] を選択します。
[リソースの 種類] を [OK ]
menu
に変更し、[OK] を 選択します。新しいメニュー フォルダーを右 クリックし 、[新規] を 選択し、[メニュー リソース ファイル] を選択します。
ファイルに名前を付け、[
drawer_menu
OK] を 選択します。ファイルが開いたら、[コード] タブを 選択して XML を表示し、コンテンツ全体を次に置き換えます。
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:showIn="navigation_view"> <group android:checkableBehavior="single"> <item android:id="@+id/nav_home" android:icon="@drawable/ic_menu_home" android:title="Home" /> <item android:id="@+id/nav_calendar" android:icon="@drawable/ic_menu_calendar" android:title="Calendar" /> <item android:id="@+id/nav_create_event" android:icon="@drawable/ic_menu_add_event" android:title="New Event" /> <item android:id="@+id/nav_signout" android:icon="@drawable/ic_menu_signout" android:title="Sign Out" /> <item android:id="@+id/nav_signin" android:icon="@drawable/ic_menu_signin" android:title="Sign In" /> </group> </menu>
アプリケーションのテーマとレイアウトを更新する
app /res/values/themes.xml ファイルを開き、要素内に次の行を追加
style
します。<item name="windowActionBar">false</item> <item name="windowNoTitle">true</item>
app /res/values-night/themes.xml ファイルを開き、要素内に次の行を追加
style
します。<item name="windowActionBar">false</item> <item name="windowNoTitle">true</item>
アプリ/ res/layout フォルダーを右クリック します。
[新規 ] を 選択し、[ レイアウト リソース ファイル] を選択します。
ファイルに名前を付
nav_header
け、 Root 要素をに 変更しLinearLayout
、[OK] を 選択します。ファイルを開 nav_header.xml 、[コード] タブ を選択 します。コンテンツ全体を次に置き換えてください。
<?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="176dp" android:background="?colorPrimary" android:gravity="bottom" android:orientation="vertical" android:padding="16dp" android:theme="@style/Theme.GraphTutorial"> <ImageView android:id="@+id/user_profile_pic" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/user_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="8dp" android:text="Test User" android:textColor="?colorOnPrimary" android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> <TextView android:id="@+id/user_email" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="test@contoso.com" android:textColor="?colorOnPrimary" /> </LinearLayout>
app /res/layout/activity_main.xml ファイルを開き、既存の XML を次に置き換え、レイアウトを a
DrawerLayout
に更新します。<?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout 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/drawer_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context=".MainActivity" tools:openDrawer="start"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <ProgressBar android:id="@+id/progressbar" android:layout_width="75dp" android:layout_height="75dp" android:layout_centerInParent="true" android:visibility="gone"/> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?colorPrimary" app:titleTextColor="?colorOnPrimary" android:elevation="4dp" android:theme="@style/Theme.GraphTutorial" /> <FrameLayout android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@+id/toolbar" /> </RelativeLayout> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_gravity="start" app:headerLayout="@layout/nav_header" app:menu="@menu/drawer_menu" /> </androidx.drawerlayout.widget.DrawerLayout>
アプリ /res/values/strings.xmlを開き、 要素内に次の要素を追加
resources
します。<string name="navigation_drawer_open">Open navigation drawer</string> <string name="navigation_drawer_close">Close navigation drawer</string>
app/java/com.example/graphtutorial/MainActivity ファイルを開き、コンテンツ全体を次に置き換えてください。
package com.example.graphtutorial; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.FrameLayout; import android.widget.ProgressBar; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import com.google.android.material.navigation.NavigationView; public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { private static final String SAVED_IS_SIGNED_IN = "isSignedIn"; private static final String SAVED_USER_NAME = "userName"; private static final String SAVED_USER_EMAIL = "userEmail"; private static final String SAVED_USER_TIMEZONE = "userTimeZone"; private DrawerLayout mDrawer; private NavigationView mNavigationView; private View mHeaderView; private boolean mIsSignedIn = false; private String mUserName = null; private String mUserEmail = null; private String mUserTimeZone = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Set the toolbar Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); mDrawer = findViewById(R.id.drawer_layout); // Add the hamburger menu icon ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, mDrawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); mDrawer.addDrawerListener(toggle); toggle.syncState(); mNavigationView = findViewById(R.id.nav_view); // Set user name and email mHeaderView = mNavigationView.getHeaderView(0); setSignedInState(mIsSignedIn); // Listen for item select events on menu mNavigationView.setNavigationItemSelectedListener(this); if (savedInstanceState == null) { // Load the home fragment by default on startup openHomeFragment(mUserName); } else { // Restore state mIsSignedIn = savedInstanceState.getBoolean(SAVED_IS_SIGNED_IN); mUserName = savedInstanceState.getString(SAVED_USER_NAME); mUserEmail = savedInstanceState.getString(SAVED_USER_EMAIL); mUserTimeZone = savedInstanceState.getString(SAVED_USER_TIMEZONE); setSignedInState(mIsSignedIn); } } @Override protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(SAVED_IS_SIGNED_IN, mIsSignedIn); outState.putString(SAVED_USER_NAME, mUserName); outState.putString(SAVED_USER_EMAIL, mUserEmail); outState.putString(SAVED_USER_TIMEZONE, mUserTimeZone); } @Override public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { // TEMPORARY return false; } @Override public void onBackPressed() { if (mDrawer.isDrawerOpen(GravityCompat.START)) { mDrawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } public void showProgressBar() { FrameLayout container = findViewById(R.id.fragment_container); ProgressBar progressBar = findViewById(R.id.progressbar); container.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); } public void hideProgressBar() { FrameLayout container = findViewById(R.id.fragment_container); ProgressBar progressBar = findViewById(R.id.progressbar); progressBar.setVisibility(View.GONE); container.setVisibility(View.VISIBLE); } // Update the menu and get the user's name and email private void setSignedInState(boolean isSignedIn) { mIsSignedIn = isSignedIn; mNavigationView.getMenu().clear(); mNavigationView.inflateMenu(R.menu.drawer_menu); Menu menu = mNavigationView.getMenu(); // Hide/show the Sign in, Calendar, and Sign Out buttons if (isSignedIn) { menu.removeItem(R.id.nav_signin); } else { menu.removeItem(R.id.nav_home); menu.removeItem(R.id.nav_calendar); menu.removeItem(R.id.nav_create_event); menu.removeItem(R.id.nav_signout); } // Set the user name and email in the nav drawer TextView userName = mHeaderView.findViewById(R.id.user_name); TextView userEmail = mHeaderView.findViewById(R.id.user_email); if (isSignedIn) { // For testing mUserName = "Lynne Robbins"; mUserEmail = "lynner@contoso.com"; mUserTimeZone = "Pacific Standard Time"; userName.setText(mUserName); userEmail.setText(mUserEmail); } else { mUserName = null; mUserEmail = null; mUserTimeZone = null; userName.setText("Please sign in"); userEmail.setText(""); } } }
フラグメントの追加
このセクションでは、ホーム ビューと予定表ビューのフラグメントを作成します。
アプリ/ res/layout フォルダーを右クリック し、[新規]、次に [ レイアウト リソース ファイル ] の順に選択します。
ファイルに名前を付
fragment_home
け、 Root 要素をに 変更しRelativeLayout
、[OK] を 選択します。ファイルを開 fragment_home.xml ファイルの内容を次に置き換えてください。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Welcome!" android:textSize="30sp" /> <TextView android:id="@+id/home_page_username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:paddingTop="8dp" android:text="Please sign in" android:textSize="20sp" /> </LinearLayout> </RelativeLayout>
アプリ/ res/layout フォルダーを右クリック し、[新規]、次に [ レイアウト リソース ファイル ] の順に選択します。
ファイルに名前を付
fragment_calendar
け、 Root 要素をに 変更しRelativeLayout
、[OK] を 選択します。ファイルを開 fragment_calendar.xml ファイルの内容を次に置き換えてください。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Calendar" android:textSize="30sp" /> </RelativeLayout>
アプリ/ res/layout フォルダーを右クリック し、[新規]、次に [ レイアウト リソース ファイル ] の順に選択します。
ファイルに名前を付
fragment_new_event
け、 Root 要素をに 変更しRelativeLayout
、[OK] を 選択します。ファイルを開 fragment_new_event.xml ファイルの内容を次に置き換えてください。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="New Event" android:textSize="30sp" /> </RelativeLayout>
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。
クラスに名前を付け
HomeFragment
、[OK] を 選択します。HomeFragment ファイルを開 き、その内容を次に置き換えてください。
package com.example.graphtutorial; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; public class HomeFragment extends Fragment { private static final String USER_NAME = "userName"; private String mUserName; public HomeFragment() { } public static HomeFragment createInstance(String userName) { HomeFragment fragment = new HomeFragment(); // Add the provided username to the fragment's arguments Bundle args = new Bundle(); args.putString(USER_NAME, userName); fragment.setArguments(args); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mUserName = getArguments().getString(USER_NAME); } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View homeView = inflater.inflate(R.layout.fragment_home, container, false); // If there is a username, replace the "Please sign in" with the username if (mUserName != null) { TextView userName = homeView.findViewById(R.id.home_page_username); userName.setText(mUserName); } return homeView; } }
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。
クラスに名前を付け
CalendarFragment
、[OK] を 選択します。CalendarFragment ファイルを開 き、その内容を次に置き換えてください。
package com.example.graphtutorial; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; public class CalendarFragment extends Fragment { private static final String TIME_ZONE = "timeZone"; private String mTimeZone; public CalendarFragment() {} public static CalendarFragment createInstance(String timeZone) { CalendarFragment fragment = new CalendarFragment(); // Add the provided time zone to the fragment's arguments Bundle args = new Bundle(); args.putString(TIME_ZONE, timeZone); fragment.setArguments(args); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mTimeZone = getArguments().getString(TIME_ZONE); } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_calendar, container, false); } }
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。
クラスに名前を付け
NewEventFragment
、[OK] を 選択します。NewEventFragment ファイルを開 き、その内容を次に置き換えてください。
package com.example.graphtutorial; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; public class NewEventFragment extends Fragment { private static final String TIME_ZONE = "timeZone"; private String mTimeZone; public NewEventFragment() {} public static NewEventFragment createInstance(String timeZone) { NewEventFragment fragment = new NewEventFragment(); // Add the provided time zone to the fragment's arguments Bundle args = new Bundle(); args.putString(TIME_ZONE, timeZone); fragment.setArguments(args); return fragment; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mTimeZone = getArguments().getString(TIME_ZONE); } } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_new_event, container, false); } }
MainActivity.java ファイルを開 き、次の関数をクラスに追加します。
// Load the "Home" fragment public void openHomeFragment(String userName) { HomeFragment fragment = HomeFragment.createInstance(userName); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment) .commit(); mNavigationView.setCheckedItem(R.id.nav_home); } // Load the "Calendar" fragment private void openCalendarFragment(String timeZone) { CalendarFragment fragment = CalendarFragment.createInstance(timeZone); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment) .commit(); mNavigationView.setCheckedItem(R.id.nav_calendar); } // Load the "New Event" fragment private void openNewEventFragment(String timeZone) { NewEventFragment fragment = NewEventFragment.createInstance(timeZone); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment) .commit(); mNavigationView.setCheckedItem(R.id.nav_create_event); } private void signIn() { setSignedInState(true); openHomeFragment(mUserName); } private void signOut() { setSignedInState(false); openHomeFragment(mUserName); }
既存の
onNavigationItemSelected
関数を、以下の関数で置き換えます。@Override public boolean onNavigationItemSelected(@NonNull MenuItem menuItem) { // Load the fragment that corresponds to the selected item switch (menuItem.getItemId()) { case R.id.nav_home: openHomeFragment(mUserName); break; case R.id.nav_calendar: openCalendarFragment(mUserTimeZone); break; case R.id.nav_create_event: openNewEventFragment(mUserTimeZone); break; case R.id.nav_signin: signIn(); break; case R.id.nav_signout: signOut(); break; } mDrawer.closeDrawer(GravityCompat.START); return true; }
すべての変更を保存します。
[実行] メニューの [アプリの 実行] を選択します。
アプリのメニューは、2 つのフラグメント間を移動し、[サインイン] または [サインアウト] ボタンをタップ すると変更されます。
ポータルでアプリを登録する
この演習では、管理センターを使用して新Azure ADネイティブ アプリケーションAzure Active Directory作成します。
ブラウザーを開き、Azure Active Directory 管理センターへ移動して、個人用アカウント (別名: Microsoft アカウント)、または 職場/学校アカウント を使用してログインします。
左側のナビゲーションで [Azure Active Directory] を選択し、それから [管理] で [アプリの登録] を選択します。
[新規登録] を選択します。 [アプリケーションを登録] ページで、次のように値を設定します。
Android Graph Tutorial
に [名前] を設定します。- [サポートされているアカウントの種類] を [任意の組織のディレクトリ内のアカウントと個人用の Microsoft アカウント] に設定します。
- [リダイレクト URI] で、ドロップダウンをパブリック クライアント/ネイティブ (モバイル & デスクトップ)
msauth://YOUR_PACKAGE_NAME/callback``YOUR_PACKAGE_NAME
に設定し、プロジェクトのパッケージ名に置き換える値を設定します。
[登録] を選択します。 [Android Graph チュートリアル] ページで、アプリケーション (クライアント) ID の値をコピーして保存します。次の手順で必要になります。
Azure AD 認証を追加する
この演習では、前の演習からアプリケーションを拡張して、アプリケーションの認証をサポートAzure AD。 これは、Microsoft サーバーを呼び出す必要がある OAuth アクセス トークンを取得するために必要Graph。 これを行うには、 Android 用 Microsoft 認証ライブラリ (MSAL) をアプリケーション に統合します。
res フォルダーを 右クリックし、[ 新規] を **選択し、[**Android リソース ディレクトリ] を選択します。
[リソースの 種類] を [OK ]
raw
に変更し、[OK] を 選択します。新しい生のフォルダーを右 クリックし、[ 新規] 、[ ファイル] の順に 選択します。
ファイルに名前を付け、[
msal_config.json
OK] を 選択します。次のファイルを msal_config.json ファイルに追加 します。
{ "client_id" : "YOUR_APP_ID_HERE", "redirect_uri" : "msauth://com.example.graphtutorial/callback", "broker_redirect_uri_registered": false, "account_mode": "SINGLE", "authorities" : [ { "type": "AAD", "audience": { "type": "AzureADandPersonalMicrosoftAccount" }, "default": true } ] }
アプリ
YOUR_APP_ID_HERE
登録のアプリ ID に置き換え、com.example.graphtutorial
プロジェクトのパッケージ名に置き換える。重要
git
msal_config.json
などのソース管理を使用している場合は、誤ってアプリ ID が漏洩しないように、ソース管理からファイルを除外する良い時期です。
サインインの実装
このセクションでは、マニフェストを更新して、MSAL がブラウザーを使用してユーザーを認証し、リダイレクト URI をアプリで処理されるとして登録し、認証ヘルパー クラスを作成し、サインインしてサインアウトするアプリを更新します。
アプリ /マニフェスト フォルダーを展開し 、アプリを 開 AndroidManifest.xml。 要素の上に次の要素を追加
application
します。<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
注意
MSAL ライブラリがユーザーを認証するには、これらのアクセス許可が必要です。
要素内に次の要素を追加
application
し、YOUR_PACKAGE_NAME_HERE
文字列をパッケージ名に置き換える。<!--Intent filter to capture authorization code response from the default browser on the device calling back to the app after interactive sign in --> <activity android:name="com.microsoft.identity.client.BrowserTabActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="msauth" android:host="YOUR_PACKAGE_NAME_HERE" android:path="/callback" /> </intent-filter> </activity>
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。 [種類] を [インターフェイス] に 変更します。 インターフェイスに名前を付け、[
IAuthenticationHelperCreatedListener
OK] を 選択します。新しいファイルを開き、その内容を次のファイルに置き換えてください。
package com.example.graphtutorial; import com.microsoft.identity.client.exception.MsalException; public interface IAuthenticationHelperCreatedListener { void onCreated(final AuthenticationHelper authHelper); void onError(final MsalException exception); }
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。 クラスに名前を付け、[
AuthenticationHelper
OK] を 選択します。新しいファイルを開き、その内容を次のファイルに置き換えてください。
package com.example.graphtutorial; import android.app.Activity; import android.content.Context; import android.util.Log; import androidx.annotation.NonNull; import com.microsoft.graph.authentication.BaseAuthenticationProvider; import com.microsoft.identity.client.AuthenticationCallback; import com.microsoft.identity.client.IAuthenticationResult; import com.microsoft.identity.client.IPublicClientApplication; import com.microsoft.identity.client.ISingleAccountPublicClientApplication; import com.microsoft.identity.client.PublicClientApplication; import com.microsoft.identity.client.exception.MsalException; import java.net.URL; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; // Singleton class - the app only needs a single instance // of PublicClientApplication public class AuthenticationHelper extends BaseAuthenticationProvider { private static AuthenticationHelper INSTANCE = null; private ISingleAccountPublicClientApplication mPCA = null; private String[] mScopes = { "User.Read", "MailboxSettings.Read", "Calendars.ReadWrite" }; private AuthenticationHelper(Context ctx, final IAuthenticationHelperCreatedListener listener) { PublicClientApplication.createSingleAccountPublicClientApplication(ctx, R.raw.msal_config, new IPublicClientApplication.ISingleAccountApplicationCreatedListener() { @Override public void onCreated(ISingleAccountPublicClientApplication application) { mPCA = application; listener.onCreated(INSTANCE); } @Override public void onError(MsalException exception) { Log.e("AUTHHELPER", "Error creating MSAL application", exception); listener.onError(exception); } }); } public static synchronized CompletableFuture<AuthenticationHelper> getInstance(Context ctx) { if (INSTANCE == null) { CompletableFuture<AuthenticationHelper> future = new CompletableFuture<>(); INSTANCE = new AuthenticationHelper(ctx, new IAuthenticationHelperCreatedListener() { @Override public void onCreated(AuthenticationHelper authHelper) { future.complete(authHelper); } @Override public void onError(MsalException exception) { future.completeExceptionally(exception); } }); return future; } else { return CompletableFuture.completedFuture(INSTANCE); } } // Version called from fragments. Does not create an // instance if one doesn't exist public static synchronized AuthenticationHelper getInstance() { if (INSTANCE == null) { throw new IllegalStateException( "AuthenticationHelper has not been initialized from MainActivity"); } return INSTANCE; } public CompletableFuture<IAuthenticationResult> acquireTokenInteractively(Activity activity) { CompletableFuture<IAuthenticationResult> future = new CompletableFuture<>(); mPCA.signIn(activity, null, mScopes, getAuthenticationCallback(future)); return future; } public CompletableFuture<IAuthenticationResult> acquireTokenSilently() { // Get the authority from MSAL config String authority = mPCA.getConfiguration() .getDefaultAuthority().getAuthorityURL().toString(); CompletableFuture<IAuthenticationResult> future = new CompletableFuture<>(); mPCA.acquireTokenSilentAsync(mScopes, authority, getAuthenticationCallback(future)); return future; } public void signOut() { mPCA.signOut(new ISingleAccountPublicClientApplication.SignOutCallback() { @Override public void onSignOut() { Log.d("AUTHHELPER", "Signed out"); } @Override public void onError(@NonNull MsalException exception) { Log.d("AUTHHELPER", "MSAL error signing out", exception); } }); } private AuthenticationCallback getAuthenticationCallback( CompletableFuture<IAuthenticationResult> future) { return new AuthenticationCallback() { @Override public void onCancel() { future.cancel(true); } @Override public void onSuccess(IAuthenticationResult authenticationResult) { future.complete(authenticationResult); } @Override public void onError(MsalException exception) { future.completeExceptionally(exception); } }; } @Nonnull @Override public CompletableFuture<String> getAuthorizationTokenAsync(@Nonnull URL requestUrl) { if (shouldAuthenticateRequestWithUrl(requestUrl) == true) { return acquireTokenSilently() .thenApply(result -> result.getAccessToken()); } return CompletableFuture.completedFuture(null); } }
MainActivity を開 き、次のステートメントを
import
追加します。import android.util.Log; import com.microsoft.identity.client.IAuthenticationResult; import com.microsoft.identity.client.exception.MsalClientException; import com.microsoft.identity.client.exception.MsalServiceException; import com.microsoft.identity.client.exception.MsalUiRequiredException;
次のメンバー プロパティをクラスに追加
MainActivity
します。private AuthenticationHelper mAuthHelper = null;
次の関数を
onCreate
関数の最後に追加します。showProgressBar(); // Get the authentication helper AuthenticationHelper.getInstance(getApplicationContext()) .thenAccept(authHelper -> { mAuthHelper = authHelper; if (!mIsSignedIn) { doSilentSignIn(false); } else { hideProgressBar(); } }) .exceptionally(exception -> { Log.e("AUTH", "Error creating auth helper", exception); return null; });
クラスに次の関数を追加
MainActivity
します。// Silently sign in - used if there is already a // user account in the MSAL cache private void doSilentSignIn(boolean shouldAttemptInteractive) { mAuthHelper.acquireTokenSilently() .thenAccept(authenticationResult -> { handleSignInSuccess(authenticationResult); }) .exceptionally(exception -> { // Check the type of exception and handle appropriately Throwable cause = exception.getCause(); if (cause instanceof MsalUiRequiredException) { Log.d("AUTH", "Interactive login required"); if (shouldAttemptInteractive) doInteractiveSignIn(); } else if (cause instanceof MsalClientException) { MsalClientException clientException = (MsalClientException)cause; if (clientException.getErrorCode() == "no_current_account" || clientException.getErrorCode() == "no_account_found") { Log.d("AUTH", "No current account, interactive login required"); if (shouldAttemptInteractive) doInteractiveSignIn(); } } else { handleSignInFailure(cause); } hideProgressBar(); return null; }); } // Prompt the user to sign in private void doInteractiveSignIn() { mAuthHelper.acquireTokenInteractively(this) .thenAccept(authenticationResult -> { handleSignInSuccess(authenticationResult); }) .exceptionally(exception -> { handleSignInFailure(exception); hideProgressBar(); return null; }); } // Handles the authentication result private void handleSignInSuccess(IAuthenticationResult authenticationResult) { // Log the token for debug purposes String accessToken = authenticationResult.getAccessToken(); Log.d("AUTH", String.format("Access token: %s", accessToken)); hideProgressBar(); setSignedInState(true); openHomeFragment(mUserName); } private void handleSignInFailure(Throwable exception) { if (exception instanceof MsalServiceException) { // Exception when communicating with the auth server, likely config issue Log.e("AUTH", "Service error authenticating", exception); } else if (exception instanceof MsalClientException) { // Exception inside MSAL, more info inside MsalError.java Log.e("AUTH", "Client error authenticating", exception); } else { Log.e("AUTH", "Unhandled exception authenticating", exception); } }
既存の関数と関数を
signIn
次signOut
に置き換える。private void signIn() { showProgressBar(); // Attempt silent sign in first // if this fails, the callback will handle doing // interactive sign in doSilentSignIn(true); } private void signOut() { mAuthHelper.signOut(); setSignedInState(false); openHomeFragment(mUserName); }
注意
メソッドがサイレント
signIn
サインイン (via) を行うのに注意してくださいdoSilentSignIn
。 サイレント メソッドが失敗した場合、このメソッドのコールバックは対話型サインインを実行します。 これにより、アプリを起動する度にユーザーにメッセージを表示する必要が回避されます。変更内容を保存し、アプリケーションを実行します。
[サインイン] メニュー項目 を タップすると、ブラウザーが [ログイン] ページAzure AD開きます。 自分のアカウントでサインインします。
アプリが再開されると、Android Studio のデバッグ ログにアクセス トークンが印刷されます。
ユーザーの詳細情報を取得する
このセクションでは、Microsoft Graph へのすべての呼び出しを保持するヘルパー クラスを作成し、MainActivity
この新しいクラスを使用してログイン ユーザーを取得するクラスを更新します。
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。 クラスに名前を付け、[
GraphHelper
OK] を 選択します。新しいファイルを開き、その内容を次のファイルに置き換えてください。
package com.example.graphtutorial; import com.microsoft.graph.models.extensions.User; import com.microsoft.graph.requests.GraphServiceClient; import java.util.concurrent.CompletableFuture; // Singleton class - the app only needs a single instance // of the Graph client public class GraphHelper implements IAuthenticationProvider { private static GraphHelper INSTANCE = null; private GraphServiceClient mClient = null; private GraphHelper() { AuthenticationHelper authProvider = AuthenticationHelper.getInstance(); mClient = GraphServiceClient.builder() .authenticationProvider(authProvider).buildClient(); } public static synchronized GraphHelper getInstance() { if (INSTANCE == null) { INSTANCE = new GraphHelper(); } return INSTANCE; } public CompletableFuture<User> getUser() { // GET /me (logged in user) return mClient.me().buildRequest() .select("displayName,mail,mailboxSettings,userPrincipalName") .getAsync(); } }
注意
このコードの動作を検討します。
- ログインしているユーザーの
getUser
情報をエンドポイントから取得する関数を/me
Graphします。- アプリケーションが
.select
必要とするユーザーのプロパティのみを要求するために使用します。
- アプリケーションが
- ログインしているユーザーの
ユーザー名とメールを設定する次の行を削除します。
// For testing mUserName = "Lynne Robbins"; mUserEmail = "lynner@contoso.com"; mUserTimeZone = "Pacific Standard Time";
既存の
handleSignInSuccess
関数を、以下の関数で置き換えます。// Handles the authentication result private void handleSignInSuccess(IAuthenticationResult authenticationResult) { // Log the token for debug purposes String accessToken = authenticationResult.getAccessToken(); Log.d("AUTH", String.format("Access token: %s", accessToken)); // Get Graph client and get user GraphHelper graphHelper = GraphHelper.getInstance(); graphHelper.getUser() .thenAccept(user -> { mUserName = user.displayName; mUserEmail = user.mail == null ? user.userPrincipalName : user.mail; mUserTimeZone = user.mailboxSettings.timeZone; runOnUiThread(() -> { hideProgressBar(); setSignedInState(true); openHomeFragment(mUserName); }); }) .exceptionally(exception -> { Log.e("AUTH", "Error getting /me", exception); runOnUiThread(()-> { hideProgressBar(); setSignedInState(false); }); return null; }); }
変更内容を保存し、アプリケーションを実行します。 サインイン後、UI はユーザーの表示名と電子メール アドレスで更新されます。
予定表ビューを取得する
この演習では、アプリケーションに Microsoft Graphを組み込む必要があります。 このアプリケーションでは、Microsoft Graph SDK を使用してJavaを呼び出Graph。
Outlook からカレンダー イベントを取得する
このセクションでは、クラスをGraphHelper``CalendarFragment
拡張して、現在の週のユーザーのイベントを取得し、これらの新しい関数を使用するために更新する関数を追加します。
GraphHelper を開 き、ファイルの上部
import
に次のステートメントを追加します。import com.microsoft.graph.options.Option; import com.microsoft.graph.options.HeaderOption; import com.microsoft.graph.options.QueryOption; import com.microsoft.graph.requests.EventCollectionPage; import com.microsoft.graph.requests.EventCollectionRequestBuilder; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CompletableFuture;
クラスに次の関数を追加
GraphHelper
します。public CompletableFuture<List<Event>> getCalendarView(ZonedDateTime viewStart, ZonedDateTime viewEnd, String timeZone) { final List<Option> options = new LinkedList<Option>(); options.add(new QueryOption("startDateTime", viewStart.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))); options.add(new QueryOption("endDateTime", viewEnd.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME))); // Start and end times adjusted to user's time zone options.add(new HeaderOption("Prefer", "outlook.timezone=\"" + timeZone + "\"")); final List<Event> allEvents = new LinkedList<Event>(); // Create a separate list of options for the paging requests // paging request should not include the query parameters from the initial // request, but should include the headers. final List<Option> pagingOptions = new LinkedList<Option>(); pagingOptions.add(new HeaderOption("Prefer", "outlook.timezone=\"" + timeZone + "\"")); return mClient.me().calendarView() .buildRequest(options) .select("subject,organizer,start,end") .orderBy("start/dateTime") .top(5) .getAsync() .thenCompose(eventPage -> processPage(eventPage, allEvents, pagingOptions)); } private CompletableFuture<List<Event>> processPage(EventCollectionPage currentPage, List<Event> eventList, List<Option> options) { eventList.addAll(currentPage.getCurrentPage()); // Check if there is another page of results EventCollectionRequestBuilder nextPage = currentPage.getNextPage(); if (nextPage != null) { // Request the next page and repeat return nextPage.buildRequest(options) .getAsync() .thenCompose(eventPage -> processPage(eventPage, eventList, options)); } else { // No more pages, complete the future // with the complete list return CompletableFuture.completedFuture(eventList); } } // Debug function to get the JSON representation of a Graph // object public String serializeObject(Object object) { return mClient.getSerializer().serializeObject(object); }
注意
コードの実行を
getCalendarView
検討します。- 呼び出される URL は
/v1.0/me/calendarview
です。- クエリ
startDateTime
パラメーターendDateTime
とクエリ パラメーターは、予定表ビューの開始と終了を定義します。 - ヘッダー
Prefer: outlook.timezone
を使用すると、Microsoft Graphユーザーのタイム ゾーン内の各イベントの開始時刻と終了時刻を返します。 select
関数は、各イベントに返されるフィールドを、ビューで実際に使用されるフィールドだけに制限します。- 関数
orderby
は、開始時刻で結果を並べ替える。 - この
top
関数は、ページごとに 25 の結果を要求します。
- クエリ
- この
processPage
関数は、使用可能な結果が多い場合はチェックし、必要に応じて追加のページを要求します。
- 呼び出される URL は
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。 クラスに名前を付け、[
GraphToIana
OK] を 選択します。新しいファイルを開き、その内容を次のファイルに置き換えてください。
package com.example.graphtutorial; import java.time.ZoneId; import java.util.HashMap; // Basic lookup for mapping Windows time zone identifiers to // IANA identifiers // Mappings taken from // https://github.com/unicode-org/cldr/blob/master/common/supplemental/windowsZones.xml public class GraphToIana { private static final HashMap<String, String> timeZoneIdMap = new HashMap<String, String>(); static { timeZoneIdMap.put("Dateline Standard Time", "Etc/GMT+12"); timeZoneIdMap.put("UTC-11", "Etc/GMT+11"); timeZoneIdMap.put("Aleutian Standard Time", "America/Adak"); timeZoneIdMap.put("Hawaiian Standard Time", "Pacific/Honolulu"); timeZoneIdMap.put("Marquesas Standard Time", "Pacific/Marquesas"); timeZoneIdMap.put("Alaskan Standard Time", "America/Anchorage"); timeZoneIdMap.put("UTC-09", "Etc/GMT+9"); timeZoneIdMap.put("Pacific Standard Time (Mexico)", "America/Tijuana"); timeZoneIdMap.put("UTC-08", "Etc/GMT+8"); timeZoneIdMap.put("Pacific Standard Time", "America/Los_Angeles"); timeZoneIdMap.put("US Mountain Standard Time", "America/Phoenix"); timeZoneIdMap.put("Mountain Standard Time (Mexico)", "America/Chihuahua"); timeZoneIdMap.put("Mountain Standard Time", "America/Denver"); timeZoneIdMap.put("Central America Standard Time", "America/Guatemala"); timeZoneIdMap.put("Central Standard Time", "America/Chicago"); timeZoneIdMap.put("Easter Island Standard Time", "Pacific/Easter"); timeZoneIdMap.put("Central Standard Time (Mexico)", "America/Mexico_City"); timeZoneIdMap.put("Canada Central Standard Time", "America/Regina"); timeZoneIdMap.put("SA Pacific Standard Time", "America/Bogota"); timeZoneIdMap.put("Eastern Standard Time (Mexico)", "America/Cancun"); timeZoneIdMap.put("Eastern Standard Time", "America/New_York"); timeZoneIdMap.put("Haiti Standard Time", "America/Port-au-Prince"); timeZoneIdMap.put("Cuba Standard Time", "America/Havana"); timeZoneIdMap.put("US Eastern Standard Time", "America/Indianapolis"); timeZoneIdMap.put("Turks And Caicos Standard Time", "America/Grand_Turk"); timeZoneIdMap.put("Paraguay Standard Time", "America/Asuncion"); timeZoneIdMap.put("Atlantic Standard Time", "America/Halifax"); timeZoneIdMap.put("Venezuela Standard Time", "America/Caracas"); timeZoneIdMap.put("Central Brazilian Standard Time", "America/Cuiaba"); timeZoneIdMap.put("SA Western Standard Time", "America/La_Paz"); timeZoneIdMap.put("Pacific SA Standard Time", "America/Santiago"); timeZoneIdMap.put("Newfoundland Standard Time", "America/St_Johns"); timeZoneIdMap.put("Tocantins Standard Time", "America/Araguaina"); timeZoneIdMap.put("E. South America Standard Time", "America/Sao_Paulo"); timeZoneIdMap.put("SA Eastern Standard Time", "America/Cayenne"); timeZoneIdMap.put("Argentina Standard Time", "America/Buenos_Aires"); timeZoneIdMap.put("Greenland Standard Time", "America/Godthab"); timeZoneIdMap.put("Montevideo Standard Time", "America/Montevideo"); timeZoneIdMap.put("Magallanes Standard Time", "America/Punta_Arenas"); timeZoneIdMap.put("Saint Pierre Standard Time", "America/Miquelon"); timeZoneIdMap.put("Bahia Standard Time", "America/Bahia"); timeZoneIdMap.put("UTC-02", "Etc/GMT+2"); timeZoneIdMap.put("Azores Standard Time", "Atlantic/Azores"); timeZoneIdMap.put("Cape Verde Standard Time", "Atlantic/Cape_Verde"); timeZoneIdMap.put("UTC", "Etc/GMT"); timeZoneIdMap.put("GMT Standard Time", "Europe/London"); timeZoneIdMap.put("Greenwich Standard Time", "Atlantic/Reykjavik"); timeZoneIdMap.put("Sao Tome Standard Time", "Africa/Sao_Tome"); timeZoneIdMap.put("Morocco Standard Time", "Africa/Casablanca"); timeZoneIdMap.put("W. Europe Standard Time", "Europe/Berlin"); timeZoneIdMap.put("Central Europe Standard Time", "Europe/Budapest"); timeZoneIdMap.put("Romance Standard Time", "Europe/Paris"); timeZoneIdMap.put("Central European Standard Time", "Europe/Warsaw"); timeZoneIdMap.put("W. Central Africa Standard Time", "Africa/Lagos"); timeZoneIdMap.put("Jordan Standard Time", "Asia/Amman"); timeZoneIdMap.put("GTB Standard Time", "Europe/Bucharest"); timeZoneIdMap.put("Middle East Standard Time", "Asia/Beirut"); timeZoneIdMap.put("Egypt Standard Time", "Africa/Cairo"); timeZoneIdMap.put("E. Europe Standard Time", "Europe/Chisinau"); timeZoneIdMap.put("Syria Standard Time", "Asia/Damascus"); timeZoneIdMap.put("West Bank Standard Time", "Asia/Hebron"); timeZoneIdMap.put("South Africa Standard Time", "Africa/Johannesburg"); timeZoneIdMap.put("FLE Standard Time", "Europe/Kiev"); timeZoneIdMap.put("Israel Standard Time", "Asia/Jerusalem"); timeZoneIdMap.put("Kaliningrad Standard Time", "Europe/Kaliningrad"); timeZoneIdMap.put("Sudan Standard Time", "Africa/Khartoum"); timeZoneIdMap.put("Libya Standard Time", "Africa/Tripoli"); timeZoneIdMap.put("Namibia Standard Time", "Africa/Windhoek"); timeZoneIdMap.put("Arabic Standard Time", "Asia/Baghdad"); timeZoneIdMap.put("Turkey Standard Time", "Europe/Istanbul"); timeZoneIdMap.put("Arab Standard Time", "Asia/Riyadh"); timeZoneIdMap.put("Belarus Standard Time", "Europe/Minsk"); timeZoneIdMap.put("Russian Standard Time", "Europe/Moscow"); timeZoneIdMap.put("E. Africa Standard Time", "Africa/Nairobi"); timeZoneIdMap.put("Iran Standard Time", "Asia/Tehran"); timeZoneIdMap.put("Arabian Standard Time", "Asia/Dubai"); timeZoneIdMap.put("Astrakhan Standard Time", "Europe/Astrakhan"); timeZoneIdMap.put("Azerbaijan Standard Time", "Asia/Baku"); timeZoneIdMap.put("Russia Time Zone 3", "Europe/Samara"); timeZoneIdMap.put("Mauritius Standard Time", "Indian/Mauritius"); timeZoneIdMap.put("Saratov Standard Time", "Europe/Saratov"); timeZoneIdMap.put("Georgian Standard Time", "Asia/Tbilisi"); timeZoneIdMap.put("Volgograd Standard Time", "Europe/Volgograd"); timeZoneIdMap.put("Caucasus Standard Time", "Asia/Yerevan"); timeZoneIdMap.put("Afghanistan Standard Time", "Asia/Kabul"); timeZoneIdMap.put("West Asia Standard Time", "Asia/Tashkent"); timeZoneIdMap.put("Ekaterinburg Standard Time", "Asia/Yekaterinburg"); timeZoneIdMap.put("Pakistan Standard Time", "Asia/Karachi"); timeZoneIdMap.put("Qyzylorda Standard Time", "Asia/Qyzylorda"); timeZoneIdMap.put("India Standard Time", "Asia/Calcutta"); timeZoneIdMap.put("Sri Lanka Standard Time", "Asia/Colombo"); timeZoneIdMap.put("Nepal Standard Time", "Asia/Katmandu"); timeZoneIdMap.put("Central Asia Standard Time", "Asia/Almaty"); timeZoneIdMap.put("Bangladesh Standard Time", "Asia/Dhaka"); timeZoneIdMap.put("Omsk Standard Time", "Asia/Omsk"); timeZoneIdMap.put("Myanmar Standard Time", "Asia/Rangoon"); timeZoneIdMap.put("SE Asia Standard Time", "Asia/Bangkok"); timeZoneIdMap.put("Altai Standard Time", "Asia/Barnaul"); timeZoneIdMap.put("W. Mongolia Standard Time", "Asia/Hovd"); timeZoneIdMap.put("North Asia Standard Time", "Asia/Krasnoyarsk"); timeZoneIdMap.put("N. Central Asia Standard Time", "Asia/Novosibirsk"); timeZoneIdMap.put("Tomsk Standard Time", "Asia/Tomsk"); timeZoneIdMap.put("China Standard Time", "Asia/Shanghai"); timeZoneIdMap.put("North Asia East Standard Time", "Asia/Irkutsk"); timeZoneIdMap.put("Singapore Standard Time", "Asia/Singapore"); timeZoneIdMap.put("W. Australia Standard Time", "Australia/Perth"); timeZoneIdMap.put("Taipei Standard Time", "Asia/Taipei"); timeZoneIdMap.put("Ulaanbaatar Standard Time", "Asia/Ulaanbaatar"); timeZoneIdMap.put("Aus Central W. Standard Time", "Australia/Eucla"); timeZoneIdMap.put("Transbaikal Standard Time", "Asia/Chita"); timeZoneIdMap.put("Tokyo Standard Time", "Asia/Tokyo"); timeZoneIdMap.put("North Korea Standard Time", "Asia/Pyongyang"); timeZoneIdMap.put("Korea Standard Time", "Asia/Seoul"); timeZoneIdMap.put("Yakutsk Standard Time", "Asia/Yakutsk"); timeZoneIdMap.put("Cen. Australia Standard Time", "Australia/Adelaide"); timeZoneIdMap.put("AUS Central Standard Time", "Australia/Darwin"); timeZoneIdMap.put("E. Australia Standard Time", "Australia/Brisbane"); timeZoneIdMap.put("AUS Eastern Standard Time", "Australia/Sydney"); timeZoneIdMap.put("West Pacific Standard Time", "Pacific/Port_Moresby"); timeZoneIdMap.put("Tasmania Standard Time", "Australia/Hobart"); timeZoneIdMap.put("Vladivostok Standard Time", "Asia/Vladivostok"); timeZoneIdMap.put("Lord Howe Standard Time", "Australia/Lord_Howe"); timeZoneIdMap.put("Bougainville Standard Time", "Pacific/Bougainville"); timeZoneIdMap.put("Russia Time Zone 10", "Asia/Srednekolymsk"); timeZoneIdMap.put("Magadan Standard Time", "Asia/Magadan"); timeZoneIdMap.put("Norfolk Standard Time", "Pacific/Norfolk"); timeZoneIdMap.put("Sakhalin Standard Time", "Asia/Sakhalin"); timeZoneIdMap.put("Central Pacific Standard Time", "Pacific/Guadalcanal"); timeZoneIdMap.put("Russia Time Zone 11", "Asia/Kamchatka"); timeZoneIdMap.put("New Zealand Standard Time", "Pacific/Auckland"); timeZoneIdMap.put("UTC+12", "Etc/GMT-12"); timeZoneIdMap.put("Fiji Standard Time", "Pacific/Fiji"); timeZoneIdMap.put("Chatham Islands Standard Time", "Pacific/Chatham"); timeZoneIdMap.put("UTC+13", "Etc/GMT-13"); timeZoneIdMap.put("Tonga Standard Time", "Pacific/Tongatapu"); timeZoneIdMap.put("Samoa Standard Time", "Pacific/Apia"); timeZoneIdMap.put("Line Islands Standard Time", "Pacific/Kiritimati"); } public static String getIanaFromWindows(String windowsTimeZone) { String iana = timeZoneIdMap.get(windowsTimeZone); // If a mapping was not found, assume the value passed // was already an IANA identifier return (iana == null) ? windowsTimeZone : iana; } public static ZoneId getZoneIdFromWindows(String windowsTimeZone) { String timeZoneId = getIanaFromWindows(windowsTimeZone); return ZoneId.of(timeZoneId); } }
import
CalendarFragment ファイルの上部に次のステートメントを追加します。import android.util.Log; import android.widget.ListView; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import com.microsoft.graph.core.ClientException; import com.microsoft.graph.models.Event; import com.microsoft.identity.client.AuthenticationCallback; import com.microsoft.identity.client.IAuthenticationResult; import com.microsoft.identity.client.exception.MsalException; import java.time.DayOfWeek; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.List;
次のメンバーをクラスに追加
CalendarFragment
します。private List<Event> mEventList = null;
次の関数をクラスに追加して
CalendarFragment
、進行状況バーを非表示にし、表示します。private void showProgressBar() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { getActivity().findViewById(R.id.progressbar) .setVisibility(View.VISIBLE); getActivity().findViewById(R.id.fragment_container) .setVisibility(View.GONE); } }); } private void hideProgressBar() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { getActivity().findViewById(R.id.progressbar) .setVisibility(View.GONE); getActivity().findViewById(R.id.fragment_container) .setVisibility(View.VISIBLE); } }); }
デバッグ目的でイベント リストを出力するには、次の関数を追加します。
private void addEventsToList() { // Temporary for debugging String jsonEvents = GraphHelper.getInstance().serializeObject(mEventList); Log.d("GRAPH", jsonEvents); }
クラスの既存の関数
onCreateView
を次に置CalendarFragment
き換える。@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_calendar, container, false); showProgressBar(); final GraphHelper graphHelper = GraphHelper.getInstance(); ZoneId tzId = GraphToIana.getZoneIdFromWindows(mTimeZone); // Get midnight of the first day of the week (assumed Sunday) // in the user's timezone, then convert to UTC ZonedDateTime startOfWeek = ZonedDateTime.now(tzId) .with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY)) .truncatedTo(ChronoUnit.DAYS) .withZoneSameInstant(ZoneId.of("UTC")); // Add 7 days to get the end of the week ZonedDateTime endOfWeek = startOfWeek.plusDays(7); // Get the user's events graphHelper .getCalendarView(startOfWeek, endOfWeek, mTimeZone) .thenAccept(eventList -> { mEventList = eventList; addEventsToList(); hideProgressBar(); }) .exceptionally(exception -> { hideProgressBar(); Log.e("GRAPH", "Error getting events", exception); Snackbar.make(getView(), exception.getMessage(), BaseTransientBottomBar.LENGTH_LONG).show(); return null; }); return view; }
アプリを実行し、サインインし、メニューの [予定表 ] ナビゲーション 項目をタップします。 Android Studio のデバッグ ログにイベントの JSON ダンプが表示されます。
結果の表示
これで、JSON ダンプを何かに置き換え、結果をユーザーフレンドリーに表示できます。 このセクションでは、カレンダー フラグメントに a ListView
Event
ListView``ListView
TextView
を追加し、内の各アイテムのレイアウトを作成し、ビュー内の各フィールドを適切にマップするカスタム リスト アダプターを作成します。
in app
TextView
/res/layout/fragment_calendar.xmlに置き換 えるListView
.<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/eventlist" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="?colorPrimary" android:dividerHeight="1dp" /> </RelativeLayout>
アプリ/ res/layout フォルダーを右クリック し、[新規]、次に [ レイアウト リソース ファイル ] の順に選択します。
ファイルに名前を付
event_list_item
け、 Root 要素をに 変更しRelativeLayout
、[OK] を 選択します。ファイルを開 event_list_item.xml ファイルの内容を次に置き換えてください。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp"> <TextView android:id="@+id/eventsubject" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Subject" android:textSize="20sp" /> <TextView android:id="@+id/eventorganizer" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/eventsubject" android:text="Adele Vance" android:textSize="15sp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/eventorganizer" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingEnd="2sp" android:text="Start:" android:textSize="15sp" android:textStyle="bold" /> <TextView android:id="@+id/eventstart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1:30 PM 2/19/2019" android:textSize="15sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingStart="5sp" android:paddingEnd="2sp" android:text="End:" android:textSize="15sp" android:textStyle="bold" /> <TextView android:id="@+id/eventend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1:30 PM 2/19/2019" android:textSize="15sp" /> </LinearLayout> </RelativeLayout>
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。
クラスに名前を付け、[
EventListAdapter
OK] を 選択します。EventListAdapter ファイルを開 き、その内容を次に置き換えてください。
package com.example.graphtutorial; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; import com.microsoft.graph.models.DateTimeTimeZone; import com.microsoft.graph.models.Event; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.List; import java.util.TimeZone; public class EventListAdapter extends ArrayAdapter<Event> { private Context mContext; private int mResource; // Used for the ViewHolder pattern // https://developer.android.com/training/improving-layouts/smooth-scrolling static class ViewHolder { TextView subject; TextView organizer; TextView start; TextView end; } public EventListAdapter(Context context, int resource, List<Event> events) { super(context, resource, events); mContext = context; mResource = resource; } @NonNull @Override public View getView(int position, View convertView, ViewGroup parent) { Event event = getItem(position); ViewHolder holder; if (convertView == null) { LayoutInflater inflater = LayoutInflater.from(mContext); convertView = inflater.inflate(mResource, parent, false); holder = new ViewHolder(); holder.subject = convertView.findViewById(R.id.eventsubject); holder.organizer = convertView.findViewById(R.id.eventorganizer); holder.start = convertView.findViewById(R.id.eventstart); holder.end = convertView.findViewById(R.id.eventend); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.subject.setText(event.subject); holder.organizer.setText(event.organizer.emailAddress.name); holder.start.setText(getLocalDateTimeString(event.start)); holder.end.setText(getLocalDateTimeString(event.end)); return convertView; } // Convert Graph's DateTimeTimeZone format to // a LocalDateTime, then return a formatted string private String getLocalDateTimeString(DateTimeTimeZone dateTime) { ZonedDateTime localDateTime = LocalDateTime.parse(dateTime.dateTime) .atZone(GraphToIana.getZoneIdFromWindows(dateTime.timeZone)); return String.format("%s %s", localDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)), localDateTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT))); } }
CalendarFragment クラスを開 き、既存の関数を次
addEventsToList
に置き換える。private void addEventsToList() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { ListView eventListView = getView().findViewById(R.id.eventlist); EventListAdapter listAdapter = new EventListAdapter(getActivity(), R.layout.event_list_item, mEventList); eventListView.setAdapter(listAdapter); } }); }
アプリを実行し、サインインし、[予定表] ナビゲーション アイテム を タップします。 イベントの一覧が表示されます。
新しいイベントを作成する
このセクションでは、ユーザーの予定表にイベントを作成する機能を追加します。
GraphHelper を開 き、ファイルの上部
import
に次のステートメントを追加します。import com.microsoft.graph.models.Attendee; import com.microsoft.graph.models.DateTimeTimeZone; import com.microsoft.graph.models.EmailAddress; import com.microsoft.graph.models.ItemBody; import com.microsoft.graph.models.AttendeeType; import com.microsoft.graph.models.BodyType;
次の関数をクラスに追加して
GraphHelper
、新しいイベントを作成します。public CompletableFuture<Event> createEvent(String subject, ZonedDateTime start, ZonedDateTime end, String timeZone, String[] attendees, String body) { Event newEvent = new Event(); // Set properties on the event // Subject newEvent.subject = subject; // Start newEvent.start = new DateTimeTimeZone(); // DateTimeTimeZone has two parts: // The date/time expressed as an ISO 8601 Local date/time // Local meaning there is no UTC or UTC offset designation // Example: 2020-01-12T09:00:00 newEvent.start.dateTime = start.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // The time zone - can be either a Windows time zone name ("Pacific Standard Time") // or an IANA time zone identifier ("America/Los_Angeles") newEvent.start.timeZone = timeZone; // End newEvent.end = new DateTimeTimeZone(); newEvent.end.dateTime = end.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); newEvent.end.timeZone = timeZone; // Add attendees if any were provided if (attendees.length > 0) { newEvent.attendees = new LinkedList<>(); for (String attendeeEmail : attendees) { Attendee newAttendee = new Attendee(); // Set the attendee type, in this case required newAttendee.type = AttendeeType.REQUIRED; // Create a new EmailAddress object with the address // provided newAttendee.emailAddress = new EmailAddress(); newAttendee.emailAddress.address = attendeeEmail; newEvent.attendees.add(newAttendee); } } // Add body if provided if (!body.isEmpty()) { newEvent.body = new ItemBody(); // Set the content newEvent.body.content = body; // Specify content is plain text newEvent.body.contentType = BodyType.TEXT; } return mClient.me().events().buildRequest() .postAsync(newEvent); }
新しいイベント フラグメントの更新
app /java/com.example.graphtutorial フォルダーを右クリックし、[ 新規] を 選択し、[クラスJava します。 クラスに名前を付け、[
EditTextDateTimePicker
OK] を 選択します。新しいファイルを開き、その内容を次のファイルに置き換えてください。
package com.example.graphtutorial; import android.app.DatePickerDialog; import android.app.TimePickerDialog; import android.content.Context; import android.view.View; import android.widget.DatePicker; import android.widget.EditText; import android.widget.TimePicker; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; // Class to wrap an EditText control to act as a date/time picker // When the user taps it, a date picker is shown, followed by a time picker // The values selected are combined to create a date/time value, which is then // displayed in the EditText public class EditTextDateTimePicker implements View.OnClickListener, DatePickerDialog.OnDateSetListener, TimePickerDialog.OnTimeSetListener { private Context mContext; private EditText mEditText; private ZonedDateTime mDateTime; EditTextDateTimePicker(Context context, EditText editText, ZoneId zoneId) { mContext = context; mEditText = editText; mEditText.setOnClickListener(this); // Initialize to now mDateTime = ZonedDateTime.now(zoneId).withSecond(0).withNano(0); // Round time to closest upcoming half-hour int offset = 30 - (mDateTime.getMinute() % 30); if (offset > 0) { mDateTime = mDateTime.plusMinutes(offset); } updateText(); } @Override public void onClick(View v) { // First, show a date picker DatePickerDialog dialog = new DatePickerDialog(mContext, this, mDateTime.getYear(), mDateTime.getMonthValue(), mDateTime.getDayOfMonth()); dialog.show(); } @Override public void onDateSet(DatePicker view, int year, int month, int dayOfMonth) { // Update the stored date/time with the new date mDateTime = mDateTime.withYear(year).withMonth(month).withDayOfMonth(dayOfMonth); // Show a time picker TimePickerDialog dialog = new TimePickerDialog(mContext, this, mDateTime.getHour(), mDateTime.getMinute(), false); dialog.show(); } @Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { // Update the stored date/time with the new time mDateTime = mDateTime.withHour(hourOfDay).withMinute(minute); // Update the text in the EditText updateText(); } public ZonedDateTime getZonedDateTime() { return mDateTime; } private void updateText() { mEditText.setText(String.format("%s %s", mDateTime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)), mDateTime.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)))); } }
このクラスは、
EditText
コントロールをラップし、ユーザーがタップした日付と時刻のピッカーを表示し、選択した日付と時刻で値を更新します。アプリ /res/layout/fragment_new_event.xmlを 開き、その内容を次に置き換えてください。
<?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" android:padding="10dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Subject" /> <com.google.android.material.textfield.TextInputLayout android:id="@+id/neweventsubject" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.google.android.material.textfield.TextInputLayout> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Attendees" /> <com.google.android.material.textfield.TextInputLayout android:id="@+id/neweventattendees" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Separate multiple entries with ';'" /> </com.google.android.material.textfield.TextInputLayout> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Start" /> <com.google.android.material.textfield.TextInputLayout android:id="@+id/neweventstartdatetime" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="false" android:clickable="true" /> </com.google.android.material.textfield.TextInputLayout> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="End" /> <com.google.android.material.textfield.TextInputLayout android:id="@+id/neweventenddatetime" android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="false" android:clickable="true" /> </com.google.android.material.textfield.TextInputLayout> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Body" /> <com.google.android.material.textfield.TextInputLayout android:id="@+id/neweventbody" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <com.google.android.material.textfield.TextInputEditText android:layout_width="match_parent" android:layout_height="match_parent" android:inputType="textMultiLine" android:gravity="top" /> </com.google.android.material.textfield.TextInputLayout> <Button android:id="@+id/createevent" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Create" /> </LinearLayout>
NewEventFragment を 開き、ファイルの
import
上部に次のステートメントを追加します。import android.util.Log; import android.widget.Button; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.textfield.TextInputLayout; import java.time.ZoneId; import java.time.ZonedDateTime;
クラスに次のメンバーを追加
NewEventFragment
します。private TextInputLayout mSubject; private TextInputLayout mAttendees; private TextInputLayout mStartInputLayout; private TextInputLayout mEndInputLayout; private TextInputLayout mBody; private EditTextDateTimePicker mStartPicker; private EditTextDateTimePicker mEndPicker;
進行状況バーを表示および非表示にする次の関数を追加します。
private void showProgressBar() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { getActivity().findViewById(R.id.progressbar) .setVisibility(View.VISIBLE); getActivity().findViewById(R.id.fragment_container) .setVisibility(View.GONE); } }); } private void hideProgressBar() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { getActivity().findViewById(R.id.progressbar) .setVisibility(View.GONE); getActivity().findViewById(R.id.fragment_container) .setVisibility(View.VISIBLE); } }); }
次の関数を追加して、入力コントロールから値を取得し、関数を呼び出
GraphHelper.createEvent
します。private void createEvent() { String subject = mSubject.getEditText().getText().toString(); String attendees = mAttendees.getEditText().getText().toString(); String body = mBody.getEditText().getText().toString(); ZonedDateTime startDateTime = mStartPicker.getZonedDateTime(); ZonedDateTime endDateTime = mEndPicker.getZonedDateTime(); // Validate boolean isValid = true; // Subject is required if (subject.isEmpty()) { isValid = false; mSubject.setError("You must set a subject"); } // End must be after start if (!endDateTime.isAfter(startDateTime)) { isValid = false; mEndInputLayout.setError("The end must be after the start"); } if (isValid) { // Split the attendees string into an array String[] attendeeArray = attendees.split(";"); GraphHelper.getInstance() .createEvent(subject, startDateTime, endDateTime, mTimeZone, attendeeArray, body) .thenAccept(newEvent -> { hideProgressBar(); Snackbar.make(getView(), "Event created", BaseTransientBottomBar.LENGTH_SHORT).show(); }) .exceptionally(exception -> { hideProgressBar(); Log.e("GRAPH", "Error creating event", exception); Snackbar.make(getView(), exception.getMessage(), BaseTransientBottomBar.LENGTH_LONG).show(); return null; }); } }
既存のファイルを次に
onCreateView
置き換える。@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View newEventView = inflater.inflate(R.layout.fragment_new_event, container, false); ZoneId userTimeZone = GraphToIana.getZoneIdFromWindows(mTimeZone); mSubject = newEventView.findViewById(R.id.neweventsubject); mAttendees = newEventView.findViewById(R.id.neweventattendees); mBody = newEventView.findViewById(R.id.neweventbody); mStartInputLayout = newEventView.findViewById(R.id.neweventstartdatetime); mStartPicker = new EditTextDateTimePicker(getContext(), mStartInputLayout.getEditText(), userTimeZone); mEndInputLayout = newEventView.findViewById(R.id.neweventenddatetime); mEndPicker = new EditTextDateTimePicker(getContext(), mEndInputLayout.getEditText(), userTimeZone); Button createButton = newEventView.findViewById(R.id.createevent); createButton.setOnClickListener(v -> { // Clear any errors mSubject.setErrorEnabled(false); mEndInputLayout.setErrorEnabled(false); showProgressBar(); createEvent(); }); return newEventView; }
変更内容を保存し、アプリを再起動します。 [新しい イベント] メニュー 項目を選択し、フォームに入力し、[CREATE] を選択 します。
おめでとうございます。
Android Microsoft のチュートリアルをGraphしました。 Microsoft Graphを呼び出す作業アプリが作成されたので、新しい機能を試して追加できます。 Microsoft Graphの概要を参照して、Microsoft Graph でアクセスできるすべてのデータを確認Graph。
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。