Merged PR 13835: Update android app with correct ui/features

Updates to UI and features were not carried over into last update
This commit is contained in:
AJ Ballway 2019-01-17 23:21:25 +00:00
Родитель e74d913b74
Коммит eca68e949b
8 изменённых файлов: 322 добавлений и 190 удалений

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

@ -1,13 +1,19 @@
package com.microsoft.connecteddevices.graphnotifications;
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
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.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
@ -28,6 +34,7 @@ import com.microsoft.connecteddevices.AsyncOperation;
import com.microsoft.connecteddevices.ConnectedDevicesAccount;
import com.microsoft.connecteddevices.ConnectedDevicesAccountType;
import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistration;
import com.microsoft.connecteddevices.EventListener;
import com.microsoft.connecteddevices.signinhelpers.AADSigninHelperAccount;
import com.microsoft.connecteddevices.signinhelpers.MSASigninHelperAccount;
import com.microsoft.connecteddevices.signinhelpers.SigninHelperAccount;
@ -39,11 +46,18 @@ import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificatio
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReader;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReaderOptions;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationStatus;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationUpdateResult;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationUserActionState;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
public class MainActivity extends AppCompatActivity {
@ -70,8 +84,12 @@ public class MainActivity extends AppCompatActivity {
private static ConnectedDevicesNotificationRegistration sNotificationRegistration;
private static UserNotificationReader sReader;
private static ArrayList<UserNotification> sNewNotifications = new ArrayList<>();
private static final ArrayList<UserNotification> sHistoricalNotifications = new ArrayList<>();
private static CountDownLatch sLatch;
private static final ArrayList<UserNotification> sNotifications = new ArrayList<>();
static final String CHANNEL_NAME = "GraphNotificationsChannel001";
private static final String NOTIFICATION_ID = "ID";
private enum LoginState {
LOGGED_IN_MSA,
@ -116,6 +134,12 @@ public class MainActivity extends AppCompatActivity {
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager));
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_NAME, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
channel.setDescription("Graph Notifications Channel");
getSystemService(NotificationManager.class).createNotificationChannel(channel);
}
if (sMSAHelperAccount == null) {
final Map<String, String[]> msaScopeOverrides = new ArrayMap<>();
msaScopeOverrides.put("https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
@ -128,6 +152,7 @@ public class MainActivity extends AppCompatActivity {
sAADHelperAccount = new AADSigninHelperAccount(Secrets.AAD_CLIENT_ID, Secrets.AAD_REDIRECT_URI, getApplicationContext());
}
sLatch = new CountDownLatch(1);
if (PlatformManager.getInstance().getPlatform() == null) {
PlatformManager.getInstance().createPlatform(getApplicationContext());
}
@ -137,6 +162,22 @@ public class MainActivity extends AppCompatActivity {
});
tryGetNotificationRegistration();
Intent intent = getIntent();
if (intent != null) {
final String id = intent.getStringExtra(NOTIFICATION_ID);
if (id != null && id.equals("")) {
new Thread(() -> {
try {
sLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
dismissNotification(id);
}).start();
}
}
}
static void tryGetNotificationRegistration() {
@ -156,6 +197,30 @@ public class MainActivity extends AppCompatActivity {
}
}
@Override
protected void onNewIntent(Intent intent) {
String id = intent.getStringExtra(NOTIFICATION_ID);
dismissNotification(id);
}
private void dismissNotification(String id) {
synchronized (sNotifications) {
boolean found = false;
for (UserNotification notification : sNotifications) {
if (notification.getId().equals(id)) {
notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
notification.saveAsync();
found = true;
break;
}
}
if (!found) {
Log.w(TAG, "Attempted to dismiss missing notification!");
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
@ -172,31 +237,23 @@ public class MainActivity extends AppCompatActivity {
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
if (id == R.id.action_refresh) {
return true;
}
return super.onOptionsItemSelected(item);
}
private static class RunnableManager {
private static Runnable sNewNotificationsUpdated;
private static Runnable sHistoryUpdated;
private static Runnable sNotificationsUpdated;
static void setNewNotificationsUpdated(Runnable runnable) {
sNewNotificationsUpdated = runnable;
static void setNotificationsUpdated(Runnable runnable) {
sNotificationsUpdated = runnable;
}
static void setHistoryUpdated(Runnable runnable) {
sHistoryUpdated = runnable;
}
static Runnable getNewNotificationsUpdated() {
return sNewNotificationsUpdated;
}
static Runnable getHistoryUpdated() {
return sHistoryUpdated;
static Runnable getNotificationsUpdated() {
return sNotificationsUpdated;
}
}
@ -312,13 +369,15 @@ public class MainActivity extends AppCompatActivity {
}
static class NotificationArrayAdapter extends ArrayAdapter<UserNotification> {
NotificationArrayAdapter(Context context, List<UserNotification> items) {
private final Activity mActivity;
NotificationArrayAdapter(Context context, List<UserNotification> items, Activity activity) {
super(context, R.layout.notifications_list_item, items);
mActivity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final UserNotification notification = sNewNotifications.get(position);
final UserNotification notification = sNotifications.get(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.notifications_list_item, parent, false);
@ -331,10 +390,35 @@ public class MainActivity extends AppCompatActivity {
String content = notification.getContent();
textView.setText(content);
convertView.setOnClickListener(view -> {
notification.setUserActionState(UserNotificationUserActionState.DISMISSED);
notification.saveAsync();
});
TextView userActionStateView = convertView.findViewById(R.id.notification_useractionstate);
userActionStateView.setText((notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION)
? "NO_INTERACTION" : "ACTIVATED");
final Button readButton = convertView.findViewById(R.id.notification_read);
if (notification.getReadState() == UserNotificationReadState.UNREAD) {
readButton.setEnabled(true);
readButton.setOnClickListener(view -> {
readButton.setEnabled(false);
notification.setReadState(UserNotificationReadState.READ);
notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
Log.d(TAG, "Successfully marked notification as read");
}
});
});
} else {
readButton.setEnabled(false);
}
if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION) {
convertView.setOnClickListener(view -> {
clearNotification(mActivity, notification.getId());
notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
notification.saveAsync();
});
} else {
convertView.setOnClickListener(null);
}
return convertView;
}
@ -356,68 +440,98 @@ public class MainActivity extends AppCompatActivity {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mNotificationArrayAdapter = new NotificationArrayAdapter(getContext(), sNewNotifications);
RunnableManager.setNewNotificationsUpdated(() -> {
Toast.makeText(getContext(), "Got a new notification!", Toast.LENGTH_SHORT).show();
mNotificationArrayAdapter = new NotificationArrayAdapter(getContext(), sNotifications, getActivity());
RunnableManager.setNotificationsUpdated(() -> {
if (getAndUpdateLoginState() != LoginState.LOGGED_OUT) {
Toast.makeText(getContext(), "Got a new notification update!", 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;
}
}
public static class LogFragment extends Fragment {
private View mRootView;
private TextView mTextView;
private File mLogFile;
private FileReader mReader;
private StringBuilder mLog = new StringBuilder();
boolean mStopReading = false;
static class HistoryArrayAdapter extends ArrayAdapter<UserNotification> {
HistoryArrayAdapter(Context context, List<UserNotification> items) {
super(context, R.layout.notifications_list_item, items);
}
public LogFragment() {}
@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(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();
public static LogFragment newInstance() {
return new LogFragment();
}
@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(() -> mHistoryArrayAdapter.notifyDataSetChanged());
ListView listView = rootView.findViewById(R.id.historyListView);
listView.setAdapter(mHistoryArrayAdapter);
return rootView;
mRootView = inflater.inflate(R.layout.fragment_log, container, false);
mTextView = mRootView.findViewById(R.id.log_text);
mLogFile = new File(getActivity().getApplicationContext().getExternalFilesDir(null), "CDPTraces.log");
try {
mReader = new FileReader(mLogFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
startReading();
return mRootView;
}
void startReading() {
new Thread(() -> {
while (!mStopReading) {
boolean stop = false;
char[] buff = new char[2048];
while (!stop) {
int readResult = -1;
try {
readResult = mReader.read(buff, 0, 2048);
} catch (IOException e) {
e.printStackTrace();
stop = true;
}
if (readResult == -1) {
stop = true;
} else {
mLog.append(buff, 0, readResult);
}
}
updateText();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
void updateText() {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
char[] endLog = new char[10000];
int length = mLog.length();
if (length > 10000) {
mLog.getChars(length - 10000, length, endLog, 0);
} else {
mLog.getChars(0, length, endLog, 0);
}
mTextView.setText(new String(endLog));
mRootView.invalidate();
});
} else {
mStopReading = true;
}
}
}
@ -428,7 +542,7 @@ public class MainActivity extends AppCompatActivity {
private class SectionsPagerAdapter extends FragmentPagerAdapter {
LoginFragment mLoginFragment;
NotificationsFragment mNotificationFragment;
HistoryFragment mHistoryFragment;
LogFragment mLogFragment;
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
@ -451,11 +565,11 @@ public class MainActivity extends AppCompatActivity {
return mNotificationFragment;
case 2:
if (mHistoryFragment == null) {
mHistoryFragment = HistoryFragment.newInstance();
if (mLogFragment == null) {
mLogFragment = LogFragment.newInstance();
}
return mHistoryFragment;
return mLogFragment;
}
return null;
@ -467,102 +581,84 @@ public class MainActivity extends AppCompatActivity {
return 3;
}
}
static void handleNotifications(final List<UserNotification> userNotifications, final Activity activity) {
activity.runOnUiThread(() -> {
synchronized (sNotifications) {
for (final UserNotification notification : userNotifications) {
for (int i = 0; i < sNotifications.size(); i++) {
if (sNotifications.get(i).getId().equals(notification.getId())) {
sNotifications.remove(i);
break;
}
}
if (notification.getStatus() == UserNotificationStatus.ACTIVE) {
sNotifications.add(0, notification);
if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION && notification.getReadState() == UserNotificationReadState.UNREAD) {
addNotification(activity, notification.getContent(), notification.getId());
} else {
clearNotification(activity, notification.getId());
}
} else {
clearNotification(activity, notification.getId());
}
}
if (RunnableManager.getNotificationsUpdated() != null) {
RunnableManager.getNotificationsUpdated().run();
}
}
});
}
static void setupChannel(final Activity activity) {
new Thread(() -> {
if (getAndUpdateLoginState() == LoginState.LOGGED_OUT){
return;
if (getAndUpdateLoginState() == LoginState.LOGGED_OUT) {
return;
}
UserDataFeed dataFeed = UserDataFeed.getForAccount(sLoggedInAccount, PlatformManager.getInstance().getPlatform(), Secrets.APP_HOST_NAME);
dataFeed.subscribeToSyncScopesAsync(Arrays.asList(UserNotificationChannel.getSyncScope())).whenCompleteAsync((success, throwable) -> {
if (success) {
dataFeed.startSync();
UserNotificationChannel channel = new UserNotificationChannel(dataFeed);
UserNotificationReaderOptions options = new UserNotificationReaderOptions();
sReader = channel.createReaderWithOptions(options);
sReader.readBatchAsync(Long.MAX_VALUE).thenAccept(userNotifications -> {
handleNotifications(userNotifications, activity);
if (sLatch.getCount() == 1) {
sLatch.countDown();
}
});
sReader.dataChanged().subscribe((userNotificationReader, aVoid) -> userNotificationReader.readBatchAsync(Long.MAX_VALUE).thenAccept(userNotifications -> {
handleNotifications(userNotifications, activity);
}));
} else {
activity.runOnUiThread(() -> Toast.makeText(activity.getApplicationContext(), "Failed to subscribe to sync scopes", Toast.LENGTH_SHORT));
}
ConnectedDevicesAccount account = sLoggedInAccount;
ArrayList<UserDataFeedSyncScope> scopes = new ArrayList<>();
scopes.add(UserNotificationChannel.getSyncScope());
UserDataFeed dataFeed = UserDataFeed.getForAccount(account, PlatformManager.getInstance().getPlatform(), Secrets.APP_HOST_NAME);
dataFeed.subscribeToSyncScopesAsync(scopes);
UserNotificationChannel channel = new UserNotificationChannel(dataFeed);
UserNotificationReaderOptions options = new UserNotificationReaderOptions();
sReader = channel.createReaderWithOptions(options);
sReader.readBatchAsync(Long.MAX_VALUE).thenAccept(userNotifications -> {
synchronized (sHistoricalNotifications) {
for (UserNotification notification : userNotifications) {
if (notification.getReadState() == UserNotificationReadState.UNREAD) {
sHistoricalNotifications.add(notification);
}
}
}
if (RunnableManager.getHistoryUpdated() != null) {
activity.runOnUiThread(RunnableManager.getHistoryUpdated());
}
});
sReader.dataChanged().subscribe((userNotificationReader, args) -> userNotificationReader.readBatchAsync(Long.MAX_VALUE).thenAccept(new AsyncOperation.ResultConsumer<List<UserNotification>>() {
@Override
public void accept(List<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:
Log.e(TAG, "Somehow got a notification with user action state " + notification.getUserActionState());
// 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();
});
}
static void addNotification(Activity activity, String message, String notificationId) {
Intent intent = new Intent(activity, MainActivity.class);
intent.putExtra(NOTIFICATION_ID, notificationId);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, PendingIntent.FLAG_ONE_SHOT);
NotificationCompat.Builder builder = new NotificationCompat.Builder(activity, MainActivity.CHANNEL_NAME)
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle("New MSGraph Notification!")
.setContentText(message)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setAutoCancel(true)
.setContentIntent(pendingIntent);
NotificationManagerCompat.from(activity).notify(notificationId.hashCode(), builder.build());
}
static void clearNotification(Activity activity, String notificationId) {
((NotificationManager)activity.getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId.hashCode());
}
}

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

@ -16,7 +16,6 @@ class Secrets {
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>>";
}

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

@ -1,12 +0,0 @@
<?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,23 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
android:id="@+id/fragment_log"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/log_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="" />
</LinearLayout>
</ScrollView>
</android.support.constraint.ConstraintLayout>

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

@ -10,6 +10,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginTop="10dp"
android:text="" />
<TextView
@ -18,6 +19,26 @@
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/notification_id"
android:layout_marginTop="10dp"
android:text="" />
<TextView
android:id="@+id/notification_useractionstate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/notification_text"
android:layout_marginTop="10dp"
android:text="" />
<Button
android:id="@+id/notification_read"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@id/notification_useractionstate"
android:layout_alignTop="@+id/notification_useractionstate"
android:layout_marginStart="10dp"
android:text="@string/read" />
</RelativeLayout>

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

@ -3,8 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.microsoft.connecteddevices.graphnotifications..MainActivity">
<item
android:id="@+id/action_settings"
android:id="@+id/action_refresh"
android:orderInCategory="100"
android:title="@string/action_settings"
android:title="@string/action_refresh"
app:showAsAction="never" />
</menu>

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

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

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

@ -1,11 +1,12 @@
<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="app_name">Graph Notifications Sample App</string>
<string name="tab_text_1">Login</string>
<string name="tab_text_2">Notifications</string>
<string name="tab_text_3">Log</string>
<string name="action_refresh">Refresh</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="login_aad">Login with AAD</string>
<string name="login_msa">Login with MSA</string>
<string name="logout">Log Out</string>
<string name="read">Read</string>
</resources>