diff --git a/README.md b/README.md index e6be8a3..3547c6e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ The Cumulocity IoT Alarming App brings alarms on your mobile phone. You'll see a Each update on an Alarm triggers a Push Notification to be sent to your device. Open the notification to quickly jump to the modified Alarm. A common use case is to inform certain users when problems occur at certain devices. *Push Notifications require a dedicated micro service to be deployed on your Cumulocity IoT tenant.* -> *The Cumulocity IoT Alarming App is currently only available for iOS 13+. An Android version will follow.* - ## Configuring Push Notifications diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..139448f --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'com.google.gms.google-services' +} + +android { + compileSdk 33 + + defaultConfig { + applicationId "com.cumulocity.alarmapp" + minSdk 24 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + dataBinding true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + implementation "com.cumulocity.client:cumulocity-kotlin-client:$cumulocityKotlinVersion@aar" + implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" + implementation "com.squareup.retrofit2:converter-scalars:$retrofitScalarsVersion" + implementation "com.squareup.retrofit2:converter-gson:$retrofitGsonVersion" + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation platform('com.google.firebase:firebase-bom:32.1.0') + implementation 'com.google.firebase:firebase-analytics' + implementation 'com.google.firebase:firebase-messaging:23.0.2' + testImplementation 'junit:junit:4.13.2' +} \ No newline at end of file diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..bbbab6d --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,39 @@ +{ + "project_info": { + "project_number": "437874919529", + "project_id": "alarmingapp-fb8dc", + "storage_bucket": "alarmingapp-fb8dc.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:437874919529:android:c0bbc86eb85b5bde17baf8", + "android_client_info": { + "package_name": "com.cumulocity.alarmapp" + } + }, + "oauth_client": [ + { + "client_id": "437874919529-b38j70qk6rb81ev4tqgrkntjhmbt0gd1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyAd5KAYDuxrMcP2QCm4sp4gdxUZ0BdJSZ0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "437874919529-b38j70qk6rb81ev4tqgrkntjhmbt0gd1.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..db5da57 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..0bf8594 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AddCommentFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/AddCommentFragment.java new file mode 100644 index 0000000..2ecba4c --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AddCommentFragment.java @@ -0,0 +1,106 @@ +package com.cumulocity.alarmapp; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; + +import com.cumulocity.alarmapp.databinding.FragmentAddCommentBinding; +import com.cumulocity.alarmapp.fragments.C8yComment; +import com.cumulocity.alarmapp.util.AlarmDetailsFilter; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.alarmapp.util.LoginHolder; +import com.cumulocity.client.model.Alarm; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.transition.MaterialContainerTransform; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class AddCommentFragment extends Fragment { + + private FragmentAddCommentBinding fragmentAddCommentBinding; + private Alarm alarm; + + private final String TAG = AddCommentFragment.class.getCanonicalName(); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + fragmentAddCommentBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_add_comment, container, false); + return fragmentAddCommentBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ArrayList list = (ArrayList) getArguments().getSerializable("AlarmSelected"); + alarm = list.get(0); + fragmentAddCommentBinding.submitButton.setEnabled(false); + fragmentAddCommentBinding.submitButton.setOnClickListener(v -> navigateToAlarmDetails()); + fragmentAddCommentBinding.inputField.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + fragmentAddCommentBinding.submitButton.setEnabled(s.toString().isEmpty() ? false : true); + } + }); + MaterialContainerTransform enterContainerTransform = new MaterialContainerTransform(requireContext(), true); + int colorSurface = MaterialColors.getColor(requireView(), com.google.android.material.R.attr.colorSurface); + enterContainerTransform.setAllContainerColors(colorSurface); + setSharedElementEnterTransition(enterContainerTransform); + AlarmDetailsFilter.getInstance().selectComments(true); + } + + private void navigateToAlarmDetails() { + updateComment(); + Navigation.findNavController(getView()).popBackStack(); + } + + private void updateComment() { + List list = new ArrayList<>(); + C8yComment[] comments = (C8yComment[]) alarm.get(C8yComment.IDENTIFIER); + C8yComment c8yComment = new C8yComment(); + c8yComment.setText(fragmentAddCommentBinding.inputField.getText().toString()); + c8yComment.setUser(LoginHolder.getInstance(MyApplication.getAppContext()).getCurrentUserName()); + list.add(c8yComment); + if (comments != null) { + list.addAll(Arrays.asList(comments)); + } + alarm.set(C8yComment.IDENTIFIER, Arrays.copyOf(list.toArray(), list.size(), C8yComment[].class)); + CumulocityAPI.Companion.getInstance().updateAlarm(alarm, alarm.getId(), new Callback() { + @Override + public void onResponse(Call call, Response response) { + Log.i(TAG, "Alarm updated: " + alarm.getId()); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed to update Alarm: " + alarm.getId()); + } + }); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AlarmDetailsFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmDetailsFragment.java new file mode 100644 index 0000000..099dcab --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmDetailsFragment.java @@ -0,0 +1,194 @@ +package com.cumulocity.alarmapp; + +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.PopupMenu; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.widget.NestedScrollView; +import androidx.databinding.DataBindingUtil; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.navigation.fragment.FragmentNavigator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.transition.TransitionManager; + +import com.cumulocity.alarmapp.databinding.FragmentAlarmDetailsBinding; +import com.cumulocity.alarmapp.fragments.C8yComment; +import com.cumulocity.alarmapp.util.AlarmDetailsFilter; +import com.cumulocity.alarmapp.util.AlarmModel; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.client.model.Alarm; +import com.google.android.material.chip.Chip; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.transition.MaterialFade; + +import java.util.ArrayList; + +import retrofit2.Callback; +import retrofit2.Response; + +/** + * A simple {@link Fragment} subclass. + * create an instance of this fragment. + */ +public class AlarmDetailsFragment extends Fragment { + + private FragmentAlarmDetailsBinding fragmentAlarmDetailsBinding; + private final String TAG = AlarmDetailsFragment.class.getCanonicalName(); + private Alarm alarm; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + fragmentAlarmDetailsBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_alarm_details, container, false); + return fragmentAlarmDetailsBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ArrayList list = (ArrayList) getArguments().getSerializable("AlarmSelected"); + alarm = list.get(0); + bind(alarm); + final boolean selected = AlarmDetailsFilter.getInstance().isCommentsSelected(); + fragmentAlarmDetailsBinding.tabLayout.getTabAt(!selected ? 0 : 1).select(); + updateUI(selected); + fragmentAlarmDetailsBinding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + updateUI(tab.getPosition() != 0); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); + } + + private void bind(Alarm alarm) { + configureDetailsCard(alarm); + configureCommentsCard(alarm); + configureChip(fragmentAlarmDetailsBinding.statusChip, R.menu.status); + configureChip(fragmentAlarmDetailsBinding.severityChip, R.menu.severity); + } + + private void configureDetailsCard(Alarm alarm) { + fragmentAlarmDetailsBinding.setVariable(BR.alarm, alarm); + fragmentAlarmDetailsBinding.setVariable(BR.severityDrawable, AlarmModel.getSeverityIcon().apply(alarm, getContext())); + fragmentAlarmDetailsBinding.setVariable(BR.statusDrawable, AlarmModel.getStatusIcon().apply(alarm, getContext())); + fragmentAlarmDetailsBinding.openDeviceButton.setOnClickListener(view -> { + Navigation.findNavController(view).navigate(R.id.actionToDeviceDetailsFragment, toBundle(alarm), null, null); + }); + } + + private void configureCommentsCard(Alarm alarm) { + final C8yComment[] comments = (C8yComment[]) alarm.get("c8y_Comments"); + CommentAdapter commentAdapter = new CommentAdapter(comments); + fragmentAlarmDetailsBinding.commentRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + fragmentAlarmDetailsBinding.commentRecyclerView.setAdapter(commentAdapter); + fragmentAlarmDetailsBinding.commentsButton.setOnClickListener(v -> { + ViewCompat.setTransitionName(fragmentAlarmDetailsBinding.commentsButton, String.valueOf(fragmentAlarmDetailsBinding.commentsButton.getId())); + FragmentNavigator.Extras extras = new FragmentNavigator.Extras.Builder() + .addSharedElement(fragmentAlarmDetailsBinding.commentsButton, "container_transformation") + .build(); + Navigation.findNavController(getView()).navigate(R.id.actionToAddCommentFragment, toBundle(alarm), null, extras); + }); + fragmentAlarmDetailsBinding.nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() { + @Override + public void onScrollChange(@NonNull NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { + if (scrollY == 0) { + fragmentAlarmDetailsBinding.commentsButton.extend(); + } else { + fragmentAlarmDetailsBinding.commentsButton.shrink(); + } + + } + }); + } + + private void validateEmptyList() { + boolean enable = fragmentAlarmDetailsBinding.commentRecyclerView.getAdapter().getItemCount() > 0; + fragmentAlarmDetailsBinding.commentRecyclerView.setVisibility(enable ? View.VISIBLE : View.GONE); + fragmentAlarmDetailsBinding.emptyView.setVisibility(enable ? View.GONE : View.VISIBLE); + } + + private void configureChip(Chip chip, int menu) { + chip.setOnClickListener(v -> showsPopUpMenu(chip, menu)); + } + + private void showsPopUpMenu(Chip chip, int menus) { + final PopupMenu popupMenu = new PopupMenu(getContext(), chip, Gravity.NO_GRAVITY, androidx.appcompat.R.attr.listPopupWindowStyle, 0); + popupMenu.getMenuInflater().inflate(menus, popupMenu.getMenu()); + int itemID = -1; + for (int i = 0; i < popupMenu.getMenu().size(); i++) { + String title = popupMenu.getMenu().getItem(i).getTitle().toString(); + if (title.equalsIgnoreCase(fragmentAlarmDetailsBinding.statusRow.getPassedText()) || title.equalsIgnoreCase(fragmentAlarmDetailsBinding.severityRow.getPassedText())) { + itemID = popupMenu.getMenu().getItem(i).getItemId(); + } + final Drawable drawable = popupMenu.getMenu().getItem(i).getIcon(); + drawable.setColorFilter(getResources().getColor(R.color.md_theme_onSurfaceVariant, null), PorterDuff.Mode.SRC_ATOP); + } + popupMenu.getMenu().removeItem(itemID); + popupMenu.setForceShowIcon(true); + popupMenu.setOnMenuItemClickListener(menuItem -> { + updateAlarm(chip, menuItem.getTitle().toString()); + return true; + }); + popupMenu.show(); + } + + private void updateAlarm(Chip chip, String title) { + if (chip.equals(fragmentAlarmDetailsBinding.statusChip)) { + Alarm.Status status = Alarm.Status.valueOf(title.toUpperCase()); + alarm.setStatus(status); + fragmentAlarmDetailsBinding.statusRow.textID.setText(title); + } else { + Alarm.Severity severity = Alarm.Severity.valueOf(title.toUpperCase()); + alarm.setSeverity(severity); + fragmentAlarmDetailsBinding.severityRow.textID.setText(title); + } + CumulocityAPI.Companion.getInstance().updateAlarm(alarm, alarm.getId(), new Callback() { + @Override + public void onResponse(retrofit2.Call call, Response response) { + AlarmDetailsFragment.this.alarm = response.body(); + bind(AlarmDetailsFragment.this.alarm); + } + + @Override + public void onFailure(retrofit2.Call call, Throwable t) { + + } + }); + } + + private static Bundle toBundle(Alarm alarm) { + ArrayList localList = new ArrayList<>(); + localList.add(alarm); + Bundle bundle = new Bundle(); + bundle.putSerializable("AlarmSelected", localList); + return bundle; + } + + private void updateUI(boolean commentSelected) { + fragmentAlarmDetailsBinding.commentsView.setVisibility(commentSelected ? View.VISIBLE : View.GONE); + fragmentAlarmDetailsBinding.detailsCardView.setVisibility(!commentSelected ? View.VISIBLE : View.GONE); + final MaterialFade animation = new MaterialFade(); + animation.addTarget(fragmentAlarmDetailsBinding.commentsButton); + TransitionManager.beginDelayedTransition((ViewGroup) fragmentAlarmDetailsBinding.getRoot(), animation); + fragmentAlarmDetailsBinding.commentsButton.setVisibility(commentSelected ? View.VISIBLE : View.GONE); + if (commentSelected) { + validateEmptyList(); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListAdapter.java b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListAdapter.java new file mode 100644 index 0000000..8e28600 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListAdapter.java @@ -0,0 +1,94 @@ +package com.cumulocity.alarmapp; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.AlarmListItemBinding; +import com.cumulocity.alarmapp.util.AlarmDetailsFilter; +import com.cumulocity.alarmapp.util.AlarmModel; +import com.cumulocity.client.model.Alarm; +import com.google.android.material.textview.MaterialTextView; + +import java.util.ArrayList; +import java.util.List; + +public class AlarmListAdapter extends RecyclerView.Adapter { + + private final List alarmList; + private final boolean displayDevices; + private AlarmListItemBinding alarmListItemBinding; + + public AlarmListAdapter(final List alarmList) { + this(alarmList, true); + } + + public AlarmListAdapter(final List alarmList, final boolean displayDevices) { + this.alarmList = alarmList; + this.displayDevices = displayDevices; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + alarmListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.alarm_list_item, parent, false); + return new ViewHolder(alarmListItemBinding.getRoot()); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + if (viewHolder instanceof ViewHolder) { + ViewHolder holder = (ViewHolder) viewHolder; + final Alarm alarm = alarmList.get(position); + alarmListItemBinding.setVariable(BR.alarm, alarm); + holder.deviceText.setVisibility(displayDevices ? View.VISIBLE : View.GONE); + + final Object[] comments = (Object[]) alarm.get("c8y_Comments"); + holder.commentImage.setVisibility(comments != null && comments.length > 0 ? View.VISIBLE : View.GONE); + + shareSeverityDrawable(alarm); + shareStatusDrawable(alarm); + holder.itemView.setOnClickListener(view -> { + ArrayList localList = new ArrayList<>(); + localList.add(alarm); + Bundle bundle = new Bundle(); + bundle.putSerializable("AlarmSelected", localList); + AlarmDetailsFilter.getInstance().selectComments(false); + Navigation.findNavController(view).navigate(R.id.actionToAlarmDetailsFragment, bundle); + }); + } + } + + private void shareStatusDrawable(Alarm alarm) { + final Context context = alarmListItemBinding.getRoot().getContext(); + alarmListItemBinding.setVariable(BR.statusDrawable, AlarmModel.getStatusIcon().apply(alarm, context)); + } + + private void shareSeverityDrawable(Alarm alarm) { + final Context context = alarmListItemBinding.getRoot().getContext(); + alarmListItemBinding.setVariable(BR.severityDrawable, AlarmModel.getSeverityIcon().apply(alarm, context)); + } + + @Override + public int getItemCount() { + return alarmList == null ? 0 : alarmList.size(); + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + public MaterialTextView deviceText = alarmListItemBinding.deviceName; + public ImageView commentImage = alarmListItemBinding.commentImage; + + ViewHolder(View v) { + super(v); + } + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFilterFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFilterFragment.java new file mode 100644 index 0000000..19a03e7 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFilterFragment.java @@ -0,0 +1,10 @@ +package com.cumulocity.alarmapp; + +import com.cumulocity.alarmapp.util.AlarmModel; +import com.cumulocity.alarmapp.util.BottomSheetListener; + +public class AlarmListFilterFragment extends BottomSheetBaseFragment { + public AlarmListFilterFragment(BottomSheetListener listener, AlarmModel alarmModel) { + super(listener, alarmModel); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFragment.java new file mode 100644 index 0000000..698d1ee --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListFragment.java @@ -0,0 +1,152 @@ +package com.cumulocity.alarmapp; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.view.MenuProvider; +import androidx.databinding.DataBindingUtil; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.FragmentAlarmListBinding; +import com.cumulocity.alarmapp.util.AlarmFilter; +import com.cumulocity.alarmapp.util.BottomSheetListener; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.client.model.Alarm; +import com.cumulocity.client.model.AlarmCollection; +import com.cumulocity.client.model.ManagedObject; +import com.cumulocity.client.model.ManagedObjectCollection; +import com.google.android.material.divider.MaterialDividerItemDecoration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * A simple {@link Fragment} subclass. + * create an instance of this fragment. + */ +public class AlarmListFragment extends Fragment implements MenuProvider, BottomSheetListener { + + private FragmentAlarmListBinding alarmListBinding; + private CumulocityAPI cumulocityAPI = CumulocityAPI.Companion.getInstance(); + private static final String TAG = AlarmListFragment.class.getCanonicalName(); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + alarmListBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_alarm_list, container, false); + return alarmListBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + RecyclerView recyclerView = alarmListBinding.recyclerView; + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + final MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(getContext(), LinearLayoutManager.VERTICAL); + divider.setLastItemDecorated(false); + recyclerView.addItemDecoration(divider); + fetchAlarms(); + getActivity().addMenuProvider(this); + } + + private void fetchAlarms() { + cumulocityAPI.filterAlarmList(AlarmFilter.getInstance(), new Callback() { + @Override + public void onResponse(Call call, Response response) { + AlarmListHeaderAdapter alarmListAdapter = new AlarmListHeaderAdapter(addHeader(Arrays.asList(response.body().getAlarms()))); + alarmListBinding.recyclerView.setAdapter(alarmListAdapter); + alarmListAdapter.notifyDataSetChanged(); + validateEmptyList(); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Alarms: " + t.getMessage()); + } + }); + } + + private void validateEmptyList() { + boolean enable = alarmListBinding.recyclerView.getAdapter().getItemCount() > 1; + alarmListBinding.emptyView.setVisibility(enable ? View.GONE : View.VISIBLE); + alarmListBinding.emptyView.findViewById(R.id.filterButton).setOnClickListener(v -> openBottomSheet()); + } + + @Override + public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { + menuInflater.inflate(R.menu.menu_alarmlist, menu); + } + + @Override + public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { + openBottomSheet(); + return true; + } + + private void openBottomSheet() { + AlarmListFilterFragment fragment = new AlarmListFilterFragment(this, AlarmFilter.getInstance()); + fragment.show(getActivity().getSupportFragmentManager(), fragment.getTag()); + } + + @Override + public void onDestroyView() { + getActivity().removeMenuProvider(this); + super.onDestroyView(); + } + + @Override + public void onBottomSheetClosed() { + AlarmFilter alarmFilter = AlarmFilter.getInstance(); + String deviceName = alarmFilter.getDeviceName(); + if (deviceName != null && !deviceName.isEmpty()) { + filterDevice(cumulocityAPI.appendNameFilter(deviceName)); + } else { + fetchAlarms(); + } + } + + private void filterDevice(String name) { + cumulocityAPI.filterDeviceName(name, new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + ManagedObjectCollection collection = response.body(); + if (collection != null && collection.getManagedObjects().length != 0) { + ManagedObject managedObject = collection.getManagedObjects()[0]; + AlarmFilter alarmFilter = AlarmFilter.getInstance(); + alarmFilter.setDeviceID(managedObject.getId()); + } + } + fetchAlarms(); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while filtering Device Name: " + t.getMessage()); + fetchAlarms(); + } + }); + } + + private List addHeader(List alarmList) { + List list = new ArrayList<>(); + //Header element at Position-0 + list.add(0, null); + list.addAll(alarmList); + return list; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListHeaderAdapter.java b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListHeaderAdapter.java new file mode 100644 index 0000000..811f029 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/AlarmListHeaderAdapter.java @@ -0,0 +1,91 @@ +package com.cumulocity.alarmapp; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.AlarmListHeaderBinding; +import com.cumulocity.alarmapp.util.AlarmFilter; +import com.cumulocity.alarmapp.util.StringUtil; +import com.cumulocity.client.model.Alarm; +import com.google.android.material.chip.Chip; + +import java.util.ArrayList; +import java.util.List; + +public class AlarmListHeaderAdapter extends AlarmListAdapter { + + private Bundle bundle; + private static final int TYPE_HEADER = 0; + private static final int TYPE_ITEM = 1; + private AlarmListHeaderBinding alarmListHeaderBinding; + + public AlarmListHeaderAdapter(List alarmList) { + super(alarmList); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == TYPE_HEADER) { + alarmListHeaderBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.alarm_list_header, parent, false); + return new HeaderHolder(alarmListHeaderBinding.getRoot()); + } + return super.onCreateViewHolder(parent, viewType); + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return TYPE_HEADER; + } + return TYPE_ITEM; + } + + public class HeaderHolder extends RecyclerView.ViewHolder { + + public HeaderHolder(@NonNull View itemView) { + super(itemView); + AlarmFilter alarmFilter = AlarmFilter.getInstance(); + configureChips(alarmFilter.getSeverity(), Alarm.Severity.values().length, R.string.label_severity_all); + configureChips(alarmFilter.getStatus(), Alarm.Status.values().length, R.string.label_status_all); + + String deviceName = alarmFilter.getDeviceName(); + if (deviceName != null && !deviceName.isEmpty()) { + addChip(deviceName); + } + String[] type = alarmFilter.getType(); + if (type != null && type.length != 0) { + for (String temp : type) { + addChip(temp); + } + } + } + } + + private void addChip(String text) { + Chip chip = new Chip(alarmListHeaderBinding.getRoot().getContext()); + chip.setText(text); + chip.setClickable(false); + alarmListHeaderBinding.chipGroup.addView(chip); + } + + private void configureChips(ArrayList list, int size, int id) { + if (list != null && list.size() != 0) { + if (list.size() == size) { + final Context context = alarmListHeaderBinding.getRoot().getContext(); + addChip(String.valueOf(context.getText(id))); + } else { + for (String temp : list) { + addChip(StringUtil.toCamelCase(temp)); + } + } + } + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/BottomSheetBaseFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/BottomSheetBaseFragment.java new file mode 100644 index 0000000..e1b79b1 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/BottomSheetBaseFragment.java @@ -0,0 +1,118 @@ +package com.cumulocity.alarmapp; + +import android.app.Dialog; +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 com.cumulocity.alarmapp.util.AlarmModel; +import com.cumulocity.alarmapp.util.BottomSheetListener; +import com.cumulocity.alarmapp.util.DashboardFilter; +import com.cumulocity.client.model.Alarm; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.chip.Chip; + +import java.util.ArrayList; + +public class BottomSheetBaseFragment extends BottomSheetDialogFragment { + View bottomSheetView; + + private BottomSheetListener bottomSheetListener; + + private AlarmModel alarmModel; + + public BottomSheetBaseFragment(BottomSheetListener listener, AlarmModel alarmModel) { + this.bottomSheetListener = listener; + this.alarmModel = alarmModel; + } + + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + BottomSheetDialog bottomSheet = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); + bottomSheetView = View.inflate(getContext(), R.layout.bottomsheet_alarmfilter_layout, null); + bottomSheet.setContentView(bottomSheetView); + BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from((View) (bottomSheetView.getParent())); + bottomSheetBehavior.setSkipCollapsed(true); + return bottomSheet; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + if (alarmModel instanceof DashboardFilter) { + bottomSheetView.findViewById(R.id.myfilterLabel).setText(getText(R.string.label_watchlist)); + bottomSheetView.findViewById(R.id.myfilterText).setText(getText(R.string.text_watchlist)); + } + updateUI(); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onDetach() { + fetchComponents(); + super.onDetach(); + } + + void updateUI() { + ArrayList severityList = alarmModel.getSeverity(); + ArrayList statusList = alarmModel.getStatus(); + + bottomSheetView.findViewById(R.id.criticalChip).setChecked(severityList.contains(Alarm.Severity.CRITICAL.name())); + bottomSheetView.findViewById(R.id.majorChip).setChecked(severityList.contains(Alarm.Severity.MAJOR.name())); + bottomSheetView.findViewById(R.id.minorChip).setChecked(severityList.contains(Alarm.Severity.MINOR.name())); + bottomSheetView.findViewById(R.id.warningChip).setChecked(severityList.contains(Alarm.Severity.WARNING.name())); + bottomSheetView.findViewById(R.id.acknowledgedChip).setChecked(statusList.contains(Alarm.Status.ACKNOWLEDGED.name())); + bottomSheetView.findViewById(R.id.clearedChip).setChecked(statusList.contains(Alarm.Status.CLEARED.name())); + bottomSheetView.findViewById(R.id.activeChip).setChecked(statusList.contains(Alarm.Status.ACTIVE.name())); + + final String[] type = alarmModel.getType(); + if (type != null && type.length > 0) { + bottomSheetView.findViewById(R.id.alarmTypeText).setText(String.join(",", type)); + } + + final String deviceName = alarmModel.getDeviceName(); + if (deviceName != null && !deviceName.isEmpty()) { + bottomSheetView.findViewById(R.id.deviceNameText).setText(deviceName); + } + } + + void fetchComponents() { + ArrayList severityList = new ArrayList(); + ArrayList statusList = new ArrayList(); + if (bottomSheetView.findViewById(R.id.criticalChip).isChecked()) { + severityList.add(Alarm.Severity.CRITICAL.name()); + } + if (bottomSheetView.findViewById(R.id.majorChip).isChecked()) { + severityList.add(Alarm.Severity.MAJOR.name()); + } + if (bottomSheetView.findViewById(R.id.minorChip).isChecked()) { + severityList.add(Alarm.Severity.MINOR.name()); + } + if (bottomSheetView.findViewById(R.id.warningChip).isChecked()) { + severityList.add(Alarm.Severity.WARNING.name()); + } + if (bottomSheetView.findViewById(R.id.activeChip).isChecked()) { + statusList.add(Alarm.Status.ACTIVE.name()); + } + if (bottomSheetView.findViewById(R.id.acknowledgedChip).isChecked()) { + statusList.add(Alarm.Status.ACKNOWLEDGED.name()); + } + if (bottomSheetView.findViewById(R.id.clearedChip).isChecked()) { + statusList.add(Alarm.Status.CLEARED.name()); + } + String type = bottomSheetView.findViewById(R.id.alarmTypeText).getText().toString(); + String[] typeArray = null; + if (type != null && !type.isEmpty()) { + typeArray = type.split(","); + } + + alarmModel.saveData(severityList, statusList, typeArray, bottomSheetView.findViewById(R.id.deviceNameText).getText().toString(), null); + bottomSheetListener.onBottomSheetClosed(); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/CommentAdapter.java b/android/app/src/main/java/com/cumulocity/alarmapp/CommentAdapter.java new file mode 100644 index 0000000..ee4ac6e --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/CommentAdapter.java @@ -0,0 +1,47 @@ +package com.cumulocity.alarmapp; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.CommentListItemBindingImpl; +import com.cumulocity.alarmapp.fragments.C8yComment; + +public class CommentAdapter extends RecyclerView.Adapter { + + private final C8yComment[] commentList; + private CommentListItemBindingImpl commentListItemBinding; + + public CommentAdapter(C8yComment[] commentList) { + this.commentList = commentList; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + commentListItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.comment_list_item, parent, false); + return new ViewHolder(commentListItemBinding.getRoot()); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + C8yComment c8yComment = commentList[position]; + commentListItemBinding.setVariable(BR.comment, c8yComment); + } + + @Override + public int getItemCount() { + return commentList == null ? 0 : commentList.length; + } + + public class ViewHolder extends RecyclerView.ViewHolder { + + ViewHolder(View v) { + super(v); + } + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFilterFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFilterFragment.java new file mode 100644 index 0000000..a44f8de --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFilterFragment.java @@ -0,0 +1,10 @@ +package com.cumulocity.alarmapp; + +import com.cumulocity.alarmapp.util.AlarmModel; +import com.cumulocity.alarmapp.util.BottomSheetListener; + +public class DashboardFilterFragment extends BottomSheetBaseFragment { + public DashboardFilterFragment(BottomSheetListener listener, AlarmModel alarmModel) { + super(listener, alarmModel); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFragment.java new file mode 100644 index 0000000..4c1e001 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/DashboardFragment.java @@ -0,0 +1,294 @@ +package com.cumulocity.alarmapp; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.view.MenuProvider; +import androidx.databinding.DataBindingUtil; +import androidx.fragment.app.Fragment; +import androidx.navigation.Navigation; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.AlarmTextTemplateBinding; +import com.cumulocity.alarmapp.databinding.FragmentWelcomeBinding; +import com.cumulocity.alarmapp.util.AlarmDetailsFilter; +import com.cumulocity.alarmapp.util.AlarmFilter; +import com.cumulocity.alarmapp.util.AlarmHolder; +import com.cumulocity.alarmapp.util.BottomSheetListener; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.alarmapp.util.DashboardFilter; +import com.cumulocity.alarmapp.util.LoginHolder; +import com.cumulocity.client.model.Alarm; +import com.cumulocity.client.model.AlarmCollection; +import com.cumulocity.client.model.ManagedObject; +import com.cumulocity.client.model.ManagedObjectCollection; +import com.cumulocity.client.supplementary.SeparatedQueryParameter; +import com.google.android.material.divider.MaterialDividerItemDecoration; + +import java.util.ArrayList; +import java.util.Arrays; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * A simple {@link Fragment} subclass. + * create an instance of this fragment. + */ +public class DashboardFragment extends Fragment implements MenuProvider, BottomSheetListener { + + private RecyclerView recyclerView; + private FragmentWelcomeBinding binding; + private CumulocityAPI cumulocityAPI = CumulocityAPI.Companion.getInstance(); + private static final String TAG = DashboardFragment.class.getCanonicalName(); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_welcome, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final String alarmId = AlarmHolder.getInstance().getAlarmId(); + if (alarmId != null && !alarmId.isEmpty()) { + fetchAlarm(alarmId); + } else { + configureAlarmsBadge(); + recyclerView = view.findViewById(R.id.recyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + final MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(getContext(), LinearLayoutManager.VERTICAL); + divider.setLastItemDecorated(false); + recyclerView.addItemDecoration(divider); + + updateDataFilter(); + fetchAlarms(); + binding.showButton.setOnClickListener(view1 -> { + navigateToAlarmList(getSeverityList()); + }); + getActivity().addMenuProvider(this); + } + } + + private void fetchAlarm(String alarmId) { + cumulocityAPI.getAlarm(alarmId, new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + AlarmHolder.getInstance().setAlarmId(null); + navigateToAlarmDetails(response.body()); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Alarm: " + t.getMessage()); + } + }); + } + + private void navigateToAlarmDetails(Alarm alarm) { + ArrayList localList = new ArrayList<>(); + localList.add(alarm); + Bundle bundle = new Bundle(); + bundle.putSerializable("AlarmSelected", localList); + AlarmDetailsFilter.getInstance().selectComments(false); + Navigation.findNavController(getView()).navigate(R.id.actionToAlarmDetailsFragment, bundle); + } + + private void fetchAlarms() { + cumulocityAPI.filterAlarmList(DashboardFilter.getInstance(getActivity()), new Callback() { + @Override + public void onResponse(Call call, Response response) { + AlarmCollection alarmCollection = response.body(); + AlarmListAdapter alarmListAdapter = new AlarmListAdapter(Arrays.asList(alarmCollection.getAlarms())); + recyclerView.setAdapter(alarmListAdapter); + alarmListAdapter.notifyDataSetChanged(); + validateEmptyList(); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Alarms: " + t.getMessage()); + } + }); + } + + private void validateEmptyList() { + boolean enable = recyclerView.getAdapter().getItemCount() > 0; + recyclerView.setVisibility(enable ? View.VISIBLE : View.GONE); + binding.filterLabel.setVisibility(enable ? View.VISIBLE : View.GONE); + binding.emptyView.setVisibility(enable ? View.GONE : View.VISIBLE); + binding.emptyView.findViewById(R.id.filterButton).setOnClickListener(v -> openBottomSheet()); + } + + private void configureAlarmsBadge() { + initializeSelection(binding.criticalLayout, Alarm.Severity.CRITICAL); + initializeSelection(binding.majorLayout, Alarm.Severity.MAJOR); + initializeSelection(binding.minorLayout, Alarm.Severity.MINOR); + initializeSelection(binding.warningLayout, Alarm.Severity.WARNING); + } + + private void initializeSelection(AlarmTextTemplateBinding layoutBinding, Alarm.Severity severity) { + layoutBinding.getRoot().setOnClickListener(view -> { + ArrayList list = new ArrayList(); + list.add(severity.name()); + navigateToAlarmList(list); + }); + cumulocityAPI.getActiveAlarmCount(new SeparatedQueryParameter(new String[]{severity.name()}), new Callback() { + @Override + public void onResponse(Call call, Response response) { + Integer value = response.body(); + layoutBinding.countText.setText(String.valueOf(value)); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Alarm count: " + t.getMessage()); + } + }); + } + + private void navigateToAlarmList(ArrayList severityList) { + AlarmFilter alarmFilter = AlarmFilter.getInstance(); + alarmFilter.saveData(severityList, getStatusList(), null, null, null); + Navigation.findNavController(getView()).navigate(R.id.actionToAlarmListFragment); + } + + private ArrayList getStatusList() { + ArrayList list = new ArrayList(); + list.add(Alarm.Status.ACTIVE.name()); + return list; + } + + private ArrayList getSeverityList() { + ArrayList list = new ArrayList(); + for (Alarm.Severity severity : Alarm.Severity.values()) { + list.add(severity.name()); + } + return list; + } + + private ArrayList getCriticalSeverity() { + ArrayList list = new ArrayList(); + list.add(Alarm.Severity.CRITICAL.name()); + return list; + } + + @Override + public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) { + menuInflater.inflate(R.menu.menu_dashboard, menu); + } + + @Override + public boolean onMenuItemSelected(@NonNull MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.logoutButton: + showLogoutDialog(); + break; + default: + openBottomSheet(); + break; + } + return true; + } + + private void openBottomSheet() { + updateDataFilter(); + DashboardFilterFragment fragment = new DashboardFilterFragment(this, DashboardFilter.getInstance(getActivity())); + fragment.show(getActivity().getSupportFragmentManager(), fragment.getTag()); + } + + @Override + public void onBottomSheetClosed() { + DashboardFilter dashboardFilter = DashboardFilter.getInstance(getActivity()); + String deviceName = dashboardFilter.getDeviceName(); + if (deviceName != null && !deviceName.isEmpty()) { + filterDevice(cumulocityAPI.appendNameFilter(deviceName)); + } else { + fetchAlarms(); + } + ((WelcomeActivity) getActivity()).postToken(LoginHolder.getInstance(getActivity()).getToken()); + } + + @Override + public void onDestroyView() { + getActivity().removeMenuProvider(this); + super.onDestroyView(); + } + + private void updateDataFilter() { + DashboardFilter dashboardFilter = DashboardFilter.getInstance(getActivity()); + if (dashboardFilter.getSeverity() == null || dashboardFilter.getStatus() == null) { + dashboardFilter.saveData(getCriticalSeverity(), getStatusList(), null, null, null); + } + } + + private void filterDevice(String name) { + cumulocityAPI.filterDeviceName(name, new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + ManagedObjectCollection collection = response.body(); + if (collection != null && collection.getManagedObjects().length != 0) { + ManagedObject managedObject = collection.getManagedObjects()[0]; + DashboardFilter dashboardFilter = DashboardFilter.getInstance(getActivity()); + dashboardFilter.setDeviceID(managedObject.getId()); + } + } + fetchAlarms(); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while filtering Device Name: " + t.getMessage()); + fetchAlarms(); + } + }); + } + + private void proceedLogout() { + cumulocityAPI.unSubscribePushNotification(LoginHolder.getInstance(getActivity()).getToken(), new Callback() { + @Override + public void onResponse(Call call, Response response) { + Log.i(TAG, "Successfully unsubscribed PushNotification: " + response.message()); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while unsubscribing PushNotification: " + t.getMessage()); + } + }); + LoginHolder.getInstance(MyApplication.getAppContext()).save(false); + navigateToLogin(); + } + + private void navigateToLogin() { + startActivity(new Intent(getActivity(), LoginActivity.class)); + getActivity().overridePendingTransition(Intent.FLAG_ACTIVITY_NO_ANIMATION, Intent.FLAG_ACTIVITY_NO_ANIMATION); + getActivity().finish(); + } + + private void showLogoutDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(getText(R.string.logout_confirmation_dialog)); + builder.setPositiveButton(getText(R.string.button_confirm), (dialog, which) -> proceedLogout()); + builder.setNegativeButton(getText(R.string.button_cancel), (dialog, which) -> dialog.dismiss()); + builder.setCancelable(false); + builder.show(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/DeviceDetailsFragment.java b/android/app/src/main/java/com/cumulocity/alarmapp/DeviceDetailsFragment.java new file mode 100644 index 0000000..9687964 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/DeviceDetailsFragment.java @@ -0,0 +1,146 @@ +package com.cumulocity.alarmapp; + +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.databinding.DataBindingUtil; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.cumulocity.alarmapp.databinding.FragmentDeviceDetailsBinding; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.client.model.Alarm; +import com.cumulocity.client.model.AlarmCollection; +import com.cumulocity.client.model.C8yHardware; +import com.cumulocity.client.model.ExternalIds; +import com.cumulocity.client.model.ManagedObject; +import com.cumulocity.client.supplementary.SeparatedQueryParameter; +import com.google.android.material.divider.MaterialDividerItemDecoration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * A simple {@link Fragment} subclass. + * create an instance of this fragment. + */ +public class DeviceDetailsFragment extends Fragment { + + private static final String C8Y_HARDWARE = "c8yHardware"; + private static final String DEVICE_TYPE = "type"; + private static final String TAG = DeviceDetailsFragment.class.getCanonicalName(); + + private final CumulocityAPI cumulocityAPI = CumulocityAPI.Companion.getInstance(); + private FragmentDeviceDetailsBinding deviceDetailsBinding; + + static { + ManagedObject.Serialization.registerAdditionalProperty(DEVICE_TYPE, String.class); + ManagedObject.Serialization.registerAdditionalProperty(C8Y_HARDWARE, C8yHardware.class); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + deviceDetailsBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_device_details, container, false); + return deviceDetailsBinding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ArrayList list = (ArrayList) getArguments().getSerializable("AlarmSelected"); + Alarm alarm = list.get(0); + fetchDeviceDetails(alarm.getSource().getId()); + fetchAlarms(alarm.getSource().getId()); + } + + private void fetchDeviceDetails(String id) { + cumulocityAPI.getDevice(id, new Callback() { + + @Override + public void onResponse(Call call, Response response) { + ManagedObject managedObject = response.body(); + if (managedObject != null) { + configureDetailsCard(managedObject); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Device details: " + t.getMessage()); + } + }); + } + + private void configureDetailsCard(ManagedObject device) { + deviceDetailsBinding.setVariable(BR.device, device); + if (device.getCustomFragments() != null) { + Map map = device.getCustomFragments(); + if (map.containsKey(C8Y_HARDWARE)) { + C8yHardware hardware = (C8yHardware) map.get(C8Y_HARDWARE); + deviceDetailsBinding.hardwareRow.getRoot().setVisibility(View.VISIBLE); + deviceDetailsBinding.setVariable(BR.C8yHardware, hardware); + } + if (map.containsKey(DEVICE_TYPE)) { + deviceDetailsBinding.typeRow.getRoot().setVisibility(View.VISIBLE); + deviceDetailsBinding.setVariable(BR.deviceType, map.get(DEVICE_TYPE).toString()); + } + } + loadExternalID(device.getId()); + } + + private void fetchAlarms(String id) { + cumulocityAPI.filterAlarms(id, new SeparatedQueryParameter(new String[]{Alarm.Status.ACTIVE.name()}), new Callback() { + @Override + public void onResponse(Call call, Response response) { + AlarmCollection alarmCollection = response.body(); + configureActiveAlarms(Arrays.asList(alarmCollection.getAlarms())); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching Alarms: " + t.getMessage()); + } + }); + } + + private void configureActiveAlarms(List list) { + RecyclerView recyclerView = getView().findViewById(R.id.recyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); + AlarmListAdapter alarmListAdapter = new AlarmListAdapter(list, false); + final MaterialDividerItemDecoration divider = new MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL); + divider.setLastItemDecorated(false); + recyclerView.addItemDecoration(divider); + recyclerView.setAdapter(alarmListAdapter); + alarmListAdapter.notifyDataSetChanged(); + } + + private void loadExternalID(String id) { + cumulocityAPI.getExternalID(id, new Callback() { + @Override + public void onResponse(Call call, Response response) { + ExternalIds externalID = response.body(); + if (externalID != null && externalID.getExternalIds().length > 0) { + deviceDetailsBinding.externalRow.getRoot().setVisibility(View.VISIBLE); + deviceDetailsBinding.setVariable(BR.externalID, externalID.getExternalIds()[0].getExternalId()); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Failed while fetching External ID: " + t.getMessage()); + } + }); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/LoginActivity.java b/android/app/src/main/java/com/cumulocity/alarmapp/LoginActivity.java new file mode 100644 index 0000000..1ce8168 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/LoginActivity.java @@ -0,0 +1,144 @@ +package com.cumulocity.alarmapp; + +import android.content.Intent; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import com.cumulocity.alarmapp.databinding.ActivityLoginBinding; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.alarmapp.util.LoginHolder; +import com.cumulocity.client.model.CurrentUser; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import javax.net.ssl.HttpsURLConnection; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class LoginActivity extends AppCompatActivity { + + private ActivityLoginBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = DataBindingUtil.setContentView(this, R.layout.activity_login); + binding.connectButton.setOnClickListener(v -> { + if (validateInputFields()) { + updateLoginInfo(); + fetchUserInfo(); + } + }); + binding.tenantField.addTextChangedListener(new CustomTextWatcher(binding.tenantField, binding.errorInputTenantField)); + binding.nameField.addTextChangedListener(new CustomTextWatcher(binding.nameField, binding.errorInputNameField)); + binding.passwordField.addTextChangedListener(new CustomTextWatcher(binding.passwordField, binding.errorInputPasswordField)); + addTemp(); + } + + private void navigateToDashboard() { + startActivity(new Intent(this, WelcomeActivity.class)); + overridePendingTransition(Intent.FLAG_ACTIVITY_NO_ANIMATION, Intent.FLAG_ACTIVITY_NO_ANIMATION); + finish(); + } + + private boolean validateInputFields() { + binding.tenantField.setText(binding.tenantField.getText().toString().trim()); + binding.nameField.setText(binding.nameField.getText().toString().trim()); + binding.passwordField.setText(binding.passwordField.getText().toString().trim()); + return !binding.tenantField.getText().toString().isEmpty() + && !binding.nameField.getText().toString().isEmpty() + && !binding.passwordField.getText().toString().isEmpty(); + } + + private class CustomTextWatcher implements TextWatcher { + + private TextInputEditText inputField; + private TextInputLayout errorTextLayout; + + CustomTextWatcher(TextInputEditText inputField, TextInputLayout errorTextLayout) { + this.inputField = inputField; + this.errorTextLayout = errorTextLayout; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable editable) { + errorTextLayout.setError(editable.toString().isEmpty() ? getString(R.string.input_field_required) : null); + } + } + + private void fetchUserInfo() { + showProgress(true); + CumulocityAPI.Companion.getInstance().getUserInfo(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + final LoginHolder loginHolder = LoginHolder.getInstance(MyApplication.getAppContext()); + loginHolder.setCurrentUserName(response.body().getUserName()); + loginHolder.save(true); + CumulocityAPI.Companion.getInstance().initializeAPIs(); + navigateToDashboard(); + } else { + showDialog(response.code() != HttpsURLConnection.HTTP_UNAUTHORIZED ? response.message() : (String) getText(R.string.login_authentication_error_message)); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + showDialog(t.getMessage()); + } + }); + } + + private void updateLoginInfo() { + LoginHolder loginHolder = LoginHolder.getInstance(MyApplication.getAppContext()); + loginHolder.setTenant(binding.tenantField.getText().toString()); + loginHolder.setUserID(binding.nameField.getText().toString()); + loginHolder.setPassword(binding.passwordField.getText().toString()); + loginHolder.save(false); + } + + private void showDialog(String msg) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getText(R.string.login_authentication_error_title)); + builder.setMessage(msg); + builder.setPositiveButton(getText(R.string.login_error_action), (dialog, which) -> { + dialog.dismiss(); + showProgress(false); + }); + builder.setCancelable(false); + builder.show(); + } + + private void showProgress(boolean value) { + binding.indicator.setVisibility(value ? View.VISIBLE : View.GONE); + binding.errorInputTenantField.setEnabled(!value); + binding.errorInputNameField.setEnabled(!value); + binding.errorInputPasswordField.setEnabled(!value); + binding.connectButton.setEnabled(!value); + } + + //Delete Later + private void addTemp() { + binding.tenantField.setText("https://mobile-jhartman.eu-latest.cumulocity.com/"); + binding.nameField.setText("test"); + binding.passwordField.setText(",Manage123"); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/MyApplication.java b/android/app/src/main/java/com/cumulocity/alarmapp/MyApplication.java new file mode 100644 index 0000000..2c1a6c7 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/MyApplication.java @@ -0,0 +1,28 @@ +package com.cumulocity.alarmapp; + +import android.app.Application; +import android.content.Context; + +import androidx.lifecycle.ProcessLifecycleOwner; + +import com.cumulocity.alarmapp.util.ApplicationLifecycleObserver; + +public class MyApplication extends Application { + private static Context context; + private static ApplicationLifecycleObserver lifecycleObserver = new ApplicationLifecycleObserver(); + + @Override + public void onCreate() { + super.onCreate(); + context = getApplicationContext(); + ProcessLifecycleOwner.get().getLifecycle().addObserver(lifecycleObserver); + } + + public static Context getAppContext() { + return context; + } + + public static boolean isAppIsInForeground() { + return lifecycleObserver.isAppIsInForeground(); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/SplashScreenActivity.java b/android/app/src/main/java/com/cumulocity/alarmapp/SplashScreenActivity.java new file mode 100644 index 0000000..25b05f7 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/SplashScreenActivity.java @@ -0,0 +1,36 @@ +package com.cumulocity.alarmapp; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; + +import androidx.appcompat.app.AppCompatActivity; + +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.alarmapp.util.LoginHolder; + +public class SplashScreenActivity extends AppCompatActivity { + + private final int SPLASH_DISPLAY_LENGTH = 1000; + private Handler handler = new Handler(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_splash_screen); + if (LoginHolder.getInstance(MyApplication.getAppContext()).isLoggedIN()) { + CumulocityAPI.Companion.getInstance().initializeAPIs(); + navigateToScreen(WelcomeActivity.class); + } else { + navigateToScreen(LoginActivity.class); + } + } + + private void navigateToScreen(Class cls) { + handler.postDelayed(() -> { + startActivity(new Intent(this, cls)); + overridePendingTransition(Intent.FLAG_ACTIVITY_NO_ANIMATION, Intent.FLAG_ACTIVITY_NO_ANIMATION); + SplashScreenActivity.this.finish(); + }, SPLASH_DISPLAY_LENGTH); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/WelcomeActivity.java b/android/app/src/main/java/com/cumulocity/alarmapp/WelcomeActivity.java new file mode 100644 index 0000000..0a26581 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/WelcomeActivity.java @@ -0,0 +1,170 @@ +package com.cumulocity.alarmapp; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.databinding.DataBindingUtil; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; + +import com.cumulocity.alarmapp.databinding.MainLayoutBinding; +import com.cumulocity.alarmapp.fragments.C8yComment; +import com.cumulocity.alarmapp.util.AlarmFilter; +import com.cumulocity.alarmapp.util.CumulocityAPI; +import com.cumulocity.alarmapp.util.DashboardFilter; +import com.cumulocity.alarmapp.util.LoginHolder; +import com.cumulocity.client.model.Alarm; +import com.cumulocity.client.model.Registration; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.firebase.messaging.FirebaseMessaging; + +import java.util.ArrayList; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class WelcomeActivity extends AppCompatActivity { + + private Toolbar toolbar; + private AppBarConfiguration appBarConfiguration; + private MainLayoutBinding mainLayoutBinding; + private static final String TAG = WelcomeActivity.class.getCanonicalName(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Alarm.Serialization.registerAdditionalProperty(C8yComment.IDENTIFIER, C8yComment[].class); + mainLayoutBinding = DataBindingUtil.setContentView(this, R.layout.main_layout); + + toolbar = mainLayoutBinding.toolbar; + setSupportActionBar(toolbar); + final NavController navController = Navigation.findNavController(this, R.id.navHostFragment); + appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); + NavigationUI.setupWithNavController(mainLayoutBinding.collapsingToolbarLayout, toolbar, navController, appBarConfiguration); + askNotificationPermission(); + } + + @Override + public boolean onSupportNavigateUp() { + final NavController navController = Navigation.findNavController(this, R.id.navHostFragment); + return NavigationUI.navigateUp(navController, appBarConfiguration) || super.onSupportNavigateUp(); + } + + @Override + protected void onDestroy() { + AlarmFilter.getInstance().deleteData(); + super.onDestroy(); + } + + private final ActivityResultLauncher requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { + if (isGranted && !LoginHolder.getInstance(this).isTokenRegistered()) { + requestToken(); + } + }); + + private void askNotificationPermission() { + final LoginHolder loginHolder = LoginHolder.getInstance(this); + if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED) { + if (loginHolder.getToken() == null || loginHolder.getToken().isEmpty()) { + requestToken(); + } else { + postToken(loginHolder.getToken()); + } + } else { + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + + private void requestToken() { + FirebaseMessaging.getInstance().getToken() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.w(TAG, "Fetching FCM registration token failed", task.getException()); + return; + } + + // Get new FCM registration token + String token = task.getResult(); + Log.i(TAG, "token: " + token); + LoginHolder.getInstance(WelcomeActivity.this).setToken(token); + postToken(token); + } + }); + } + + public void postToken(String token) { + if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED || LoginHolder.getInstance(this).getToken().isEmpty()) { + return; + } + Registration.Device device = new Registration.Device(Build.DEVICE, token, Registration.Device.Platform.ANDROID); + String userID = LoginHolder.getInstance(this).getCurrentUserName(); + Registration registration = new Registration(userID, getPackageName(), device); + registration.setTags(getTag()); + CumulocityAPI.Companion.getInstance().subscribePushNotification(registration, new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + LoginHolder.getInstance(WelcomeActivity.this).setTokenRegistered(true); + } + Log.i(TAG, "response: " + response.message()); + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "response: " + t.getMessage()); + } + }); + } + + private String[] getTag() { + DashboardFilter filter = DashboardFilter.getInstance(this); + ArrayList list = new ArrayList<>(); + + ArrayList severityList = filter.getSeverity(); + if (severityList.size() == Alarm.Severity.values().length) { + list.add("severity:all"); + } else { + for (String str : severityList) { + list.add("severity:" + str.toLowerCase()); + } + } + + ArrayList statusList = filter.getStatus(); + if (statusList.size() == Alarm.Status.values().length) { + list.add("status:all"); + } else { + for (String str : statusList) { + list.add("status:" + str.toLowerCase()); + } + } + + list.add("deviceId:" + (filter.getDeviceID() != null && !filter.getDeviceID().isEmpty() ? filter.getDeviceID() : "all")); + if (filter.getType() == null || filter.getType().length == 0) { + list.add("type:all"); + } else { + for (String str : filter.getType()) { + list.add("type:" + str); + } + } + return list.toArray(new String[list.size()]); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/datetime/C8yDateFormatter.java b/android/app/src/main/java/com/cumulocity/alarmapp/datetime/C8yDateFormatter.java new file mode 100644 index 0000000..4abb347 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/datetime/C8yDateFormatter.java @@ -0,0 +1,30 @@ +package com.cumulocity.alarmapp.datetime; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class C8yDateFormatter { + + private static final DateFormat alarmDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final DateFormat printFormat = new SimpleDateFormat("MMM dd yyyy, HH:mm"); + + private C8yDateFormatter() { + // static + } + + public static String toReadableDate(final String dateTime) { + try { + final Date date = alarmDateFormat.parse(dateTime); + return toReadableDate(date); + } catch (ParseException e) { + e.printStackTrace(); + return dateTime; + } + } + + public static String toReadableDate(final Date date) { + return printFormat.format(date); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/fragments/C8yComment.java b/android/app/src/main/java/com/cumulocity/alarmapp/fragments/C8yComment.java new file mode 100644 index 0000000..e6c8ead --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/fragments/C8yComment.java @@ -0,0 +1,38 @@ +package com.cumulocity.alarmapp.fragments; + +import com.cumulocity.alarmapp.datetime.C8yDateFormatter; + +import java.util.Date; + +public class C8yComment { + + public static final String IDENTIFIER = "c8y_Comments"; + + private String text; + private String user; + private String time; + + public C8yComment() { + this.time = C8yDateFormatter.toReadableDate(new Date()); + } + + public String getText() { + return text; + } + + public String getUser() { + return user; + } + + public String getTime() { + return time; + } + + public void setText(String text) { + this.text = text; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/service/AppPushNotificationService.java b/android/app/src/main/java/com/cumulocity/alarmapp/service/AppPushNotificationService.java new file mode 100644 index 0000000..aa37314 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/service/AppPushNotificationService.java @@ -0,0 +1,40 @@ +package com.cumulocity.alarmapp.service; + +import android.content.Intent; +import android.os.Bundle; +import android.os.Looper; +import android.widget.Toast; + +import androidx.annotation.NonNull; + +import com.cumulocity.alarmapp.MyApplication; +import com.cumulocity.alarmapp.util.AlarmHolder; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; + +public class AppPushNotificationService extends FirebaseMessagingService { + + @Override + public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { + super.onMessageReceived(remoteMessage); + Looper.prepare(); + Toast.makeText(MyApplication.getAppContext(), remoteMessage.getNotification().getBody(), Toast.LENGTH_SHORT).show(); + } + + @Override + public void handleIntent(Intent intent) { + super.handleIntent(intent); + final Bundle data = intent.getExtras(); + final RemoteMessage remoteMessage = new RemoteMessage(data); + if (!MyApplication.isAppIsInForeground()) { + retrieveAlarmId(remoteMessage); + } + } + + private void retrieveAlarmId(RemoteMessage remoteMessage) { + String alarmId = remoteMessage.getData().get("alarmId"); + if (alarmId != null && !alarmId.isEmpty()) { + AlarmHolder.getInstance().setAlarmId(alarmId); + } + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmDetailsFilter.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmDetailsFilter.java new file mode 100644 index 0000000..1209f1b --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmDetailsFilter.java @@ -0,0 +1,25 @@ +package com.cumulocity.alarmapp.util; + +public class AlarmDetailsFilter { + private static AlarmDetailsFilter instance; + private boolean tabSelected; + + private AlarmDetailsFilter() { + + } + + public static AlarmDetailsFilter getInstance() { + if (instance == null) { + instance = new AlarmDetailsFilter(); + } + return instance; + } + + public void selectComments(boolean value) { + tabSelected = value; + } + + public boolean isCommentsSelected() { + return tabSelected; + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmFilter.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmFilter.java new file mode 100644 index 0000000..7e003be --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmFilter.java @@ -0,0 +1,16 @@ +package com.cumulocity.alarmapp.util; + +public final class AlarmFilter extends AlarmModel { + private static AlarmFilter instance; + + private AlarmFilter() { + + } + + public static AlarmFilter getInstance() { + if (instance == null) { + instance = new AlarmFilter(); + } + return instance; + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmHolder.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmHolder.java new file mode 100644 index 0000000..e2889d8 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmHolder.java @@ -0,0 +1,27 @@ +package com.cumulocity.alarmapp.util; + +public class AlarmHolder { + + private static AlarmHolder instance; + private String alarmId; + + private AlarmHolder() { + + } + + public static AlarmHolder getInstance() { + if (instance == null) { + instance = new AlarmHolder(); + } + return instance; + } + + public String getAlarmId() { + return alarmId; + } + + public void setAlarmId(String alarmId) { + this.alarmId = alarmId; + } + +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmModel.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmModel.java new file mode 100644 index 0000000..388e137 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/AlarmModel.java @@ -0,0 +1,89 @@ +package com.cumulocity.alarmapp.util; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import androidx.appcompat.content.res.AppCompatResources; + +import com.cumulocity.alarmapp.R; +import com.cumulocity.client.model.Alarm; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.function.BiFunction; + +public class AlarmModel { + private ArrayList severity; + private ArrayList status; + private String[] type; + private String deviceID, deviceName; + + public ArrayList getSeverity() { + return severity; + } + + public ArrayList getStatus() { + return status; + } + + public String[] getType() { + return type; + } + + public String getDeviceID() { + return deviceID; + } + + public void setDeviceID(String deviceID) { + this.deviceID = deviceID; + } + + public String getDeviceName() { + return deviceName; + } + + public synchronized void saveData(ArrayList severity, ArrayList status, String[] type, + String deviceName, String deviceID) { + this.severity = severity; + this.status = status; + this.type = type; + this.deviceName = deviceName; + this.deviceID = deviceID; + } + + public synchronized void deleteData() { + this.severity = null; + this.status = null; + this.type = null; + this.deviceID = null; + this.deviceName = null; + } + + public static BiFunction getStatusIcon() { + return (alarm, context) -> { + switch (Objects.requireNonNull(alarm.getStatus())) { + case ACTIVE: + return AppCompatResources.getDrawable(context, R.drawable.ic_status_active); + case ACKNOWLEDGED: + return AppCompatResources.getDrawable(context, R.drawable.ic_status_acknowledged); + default: + return AppCompatResources.getDrawable(context, R.drawable.ic_status_cleared); + } + }; + } + + public static BiFunction getSeverityIcon() { + return (alarm, context) -> { + switch (Objects.requireNonNull(alarm.getSeverity())) { + case CRITICAL: + return AppCompatResources.getDrawable(context, R.drawable.ic_severity_critical); + case MAJOR: + return AppCompatResources.getDrawable(context, R.drawable.ic_severity_major); + case MINOR: + return AppCompatResources.getDrawable(context, R.drawable.ic_severity_minor); + default: + return AppCompatResources.getDrawable(context, R.drawable.ic_severity_warning); + } + }; + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/ApplicationLifecycleObserver.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/ApplicationLifecycleObserver.java new file mode 100644 index 0000000..bbe3c09 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/ApplicationLifecycleObserver.java @@ -0,0 +1,24 @@ +package com.cumulocity.alarmapp.util; + +import androidx.annotation.NonNull; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; + +public class ApplicationLifecycleObserver implements LifecycleEventObserver { + + private boolean appIsInForeground; + + public boolean isAppIsInForeground() { + return appIsInForeground; + } + + @Override + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { + if (event == Lifecycle.Event.ON_START || event == Lifecycle.Event.ON_RESUME) { + appIsInForeground = true; + } else if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) { + appIsInForeground = false; + } + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/BottomSheetListener.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/BottomSheetListener.java new file mode 100644 index 0000000..b81a39d --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/BottomSheetListener.java @@ -0,0 +1,5 @@ +package com.cumulocity.alarmapp.util; + +public interface BottomSheetListener { + public void onBottomSheetClosed(); +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/CumulocityAPI.kt b/android/app/src/main/java/com/cumulocity/alarmapp/util/CumulocityAPI.kt new file mode 100644 index 0000000..50b9617 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/CumulocityAPI.kt @@ -0,0 +1,131 @@ +package com.cumulocity.alarmapp.util + +import com.cumulocity.alarmapp.MyApplication +import com.cumulocity.client.api.* +import com.cumulocity.client.model.* +import com.cumulocity.client.supplementary.SeparatedQueryParameter +import okhttp3.Credentials +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Callback +import java.util.concurrent.TimeUnit +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLSession + +class CumulocityAPI private constructor() { + private lateinit var alarmsApi: AlarmsApi + private lateinit var managedObjectsApi: ManagedObjectsApi + private lateinit var externalIDsApi: ExternalIDsApi + private lateinit var currentUserApi: CurrentUserApi + private lateinit var registrationApi: RegistrationApi; + + private fun createHttpClientBuilder(): OkHttpClient.Builder { + val client = createOkHttpClient() + val interceptor = Interceptor { chain: Interceptor.Chain -> + val authToken = Credentials.basic( + LoginHolder.getInstance(MyApplication.getAppContext()).userID, + LoginHolder.getInstance(MyApplication.getAppContext()).password + ) + var request = chain.request() + val headers = request.headers().newBuilder().add("Authorization", authToken).build() + request = request.newBuilder().headers(headers).build() + chain.proceed(request) + } + client.addInterceptor(interceptor) + return client + } + + private fun createOkHttpClient(): OkHttpClient.Builder { + return OkHttpClient.Builder() + .readTimeout(1266666, TimeUnit.MILLISECONDS) + .hostnameVerifier { hostname: String?, sslSession: SSLSession? -> + HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, sslSession) + } + } + + fun getActiveAlarmCount(severity: SeparatedQueryParameter, callback: Callback) { + alarmsApi.getNumberOfAlarms( + severity = severity, + status = SeparatedQueryParameter(arrayOf(Alarm.Status.ACTIVE.name)) + ).enqueue(callback) + } + + fun getDevice(id: String, callback: Callback) { + managedObjectsApi.getManagedObject(id = id).enqueue(callback) + } + + fun filterAlarmList(alarmModel: AlarmModel, callback: Callback) { + alarmsApi.getAlarms( + pageSize = 50, + severity = SeparatedQueryParameter(alarmModel.severity.toTypedArray()), + status = SeparatedQueryParameter(alarmModel.status.toTypedArray()), + type = SeparatedQueryParameter(alarmModel.type), + source = alarmModel.deviceID + ).enqueue(callback) + } + + fun filterAlarms( + sourceID: String, + status: SeparatedQueryParameter, + callback: Callback + ) { + alarmsApi.getAlarms(pageSize = 50, source = sourceID, status = status).enqueue(callback) + } + + fun getExternalID(id: String, callback: Callback) { + externalIDsApi.getExternalIds(id).enqueue(callback) + } + + fun filterDeviceName(query: String, callback: Callback) { + managedObjectsApi.getManagedObjects(query = query).enqueue(callback) + } + + fun appendNameFilter(deviceName: String): String { + return "\$filter=name eq $deviceName" + } + + fun updateAlarm(alarm: Alarm, id: String, callback: Callback) { + alarmsApi.updateAlarm(body = alarm, id = id).enqueue(callback); + } + + fun getUserInfo(callback: Callback) { + currentUserApi = CurrentUserApi.create( + LoginHolder.getInstance(MyApplication.getAppContext()).tenant, + createHttpClientBuilder() + ) + currentUserApi.getCurrentUser().enqueue(callback); + } + + fun subscribePushNotification(registration: Registration, callback: Callback) { + registrationApi.subscribe(registration).enqueue(callback); + } + + fun unSubscribePushNotification(deviceToken: String, callback: Callback) { + registrationApi.unsubscribe(deviceToken = deviceToken).enqueue(callback); + } + + fun getAlarm(alarmId: String, callback: Callback) { + alarmsApi.getAlarm(id = alarmId).enqueue(callback); + } + + fun initializeAPIs() { + val URL = LoginHolder.getInstance(MyApplication.getAppContext()).tenant; + val httpClientBuilder = createHttpClientBuilder() + alarmsApi = AlarmsApi.create(URL, httpClientBuilder) + managedObjectsApi = ManagedObjectsApi.create(URL, httpClientBuilder) + externalIDsApi = ExternalIDsApi.create(URL, httpClientBuilder) + registrationApi = RegistrationApi.create(URL, httpClientBuilder) + } + + companion object { + var instance: CumulocityAPI? = null + get() { + if (field == null) { + field = CumulocityAPI() + } + return field + } + private set + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/DashboardFilter.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/DashboardFilter.java new file mode 100644 index 0000000..3384a21 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/DashboardFilter.java @@ -0,0 +1,37 @@ +package com.cumulocity.alarmapp.util; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.ArrayList; + +import retrofit2.converter.gson.ExtendedGsonConverterFactory; + +public final class DashboardFilter extends AlarmModel { + + private static DashboardFilter instance; + private static SharedPreferences sharedPreferences; + + private final static String FILTER = "DashboardFilter"; + private final static ExtendedGsonConverterFactory extendedGsonConverterFactory = new ExtendedGsonConverterFactory(); + + private DashboardFilter(Context context) { + sharedPreferences = context.getSharedPreferences(FILTER, Context.MODE_PRIVATE); + } + + public static DashboardFilter getInstance(Context context) { + if (instance == null) { + instance = new DashboardFilter(context); + } + String temp = sharedPreferences.getString(FILTER, null); + return temp != null ? extendedGsonConverterFactory.getGson().fromJson(temp, DashboardFilter.class) : instance; + } + + @Override + public synchronized void saveData(ArrayList severity, ArrayList status, String[] type, String deviceName, String deviceID) { + super.saveData(severity, status, type, deviceName, deviceID); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(FILTER, extendedGsonConverterFactory.getGson().toJson(this)); + editor.commit(); + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/LoginHolder.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/LoginHolder.java new file mode 100644 index 0000000..fa2dcf3 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/LoginHolder.java @@ -0,0 +1,104 @@ +package com.cumulocity.alarmapp.util; + +import android.content.Context; +import android.content.SharedPreferences; + +import retrofit2.converter.gson.ExtendedGsonConverterFactory; + +public class LoginHolder { + private String currentUserName; + private String tenant; + private String userID; + private String password; + + private String token; + + private boolean tokenRegistered; + + private static LoginHolder holder; + private static SharedPreferences sharedPreferences; + private final static ExtendedGsonConverterFactory extendedGsonConverterFactory = new ExtendedGsonConverterFactory(); + + private final static String KEY = "LoginHolder"; + private boolean loggedIN = false; + + private LoginHolder(Context context) { + sharedPreferences = context.getSharedPreferences(KEY, Context.MODE_PRIVATE); + } + + public static LoginHolder getInstance(Context context) { + if (holder == null) { + holder = new LoginHolder(context); + } + String temp = sharedPreferences.getString(KEY, null); + return temp != null ? extendedGsonConverterFactory.getGson().fromJson(temp, LoginHolder.class) : holder; + } + + public void setCurrentUserName(String name) { + this.currentUserName = name; + } + + public String getCurrentUserName() { + return currentUserName; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getUserID() { + return userID; + } + + public void setUserID(String userID) { + this.userID = userID; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public void save(boolean value) { + loggedIN = value; + if (!value) { + tokenRegistered = false; + } + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(KEY, extendedGsonConverterFactory.getGson().toJson(this)); + editor.commit(); + } + + public boolean isLoggedIN() { + return loggedIN; + } + + public void setTokenRegistered(boolean value) { + tokenRegistered = value; + if (value) { + save(true); + } + } + + public boolean isTokenRegistered() { + return tokenRegistered; + } + + public void setToken(String value) { + token = value; + if (!value.isEmpty()) { + save(true); + } + } + + public String getToken() { + return token; + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/Registration.kt b/android/app/src/main/java/com/cumulocity/alarmapp/util/Registration.kt new file mode 100644 index 0000000..4cd1207 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/Registration.kt @@ -0,0 +1,38 @@ +// Copyright (c) 2014-2023 Software AG, Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA, and/or its subsidiaries and/or its affiliates and/or their licensors. +// Use, reproduction, transfer, publication or disclosure is prohibited except as specifically provided for in your License Agreement with Software AG. + +package com.cumulocity.client.model +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName + +data class Registration(var userId: String?, var bundleId: String?, var device: Device?) { + constructor() : this(userId = null, bundleId = null, device = null) + + /** + * Additional information for this subscription. + */ + var tags: Array? = null + + /** + * Device information. + */ + data class Device(var deviceName: String?, var deviceToken: String?, var platform: Platform?) { + constructor() : this(deviceName = null, deviceToken = null, platform = null) + + enum class Platform(val value: String) { + @SerializedName(value = "IOS") + IOS("IOS"), + @SerializedName(value = "Android") + ANDROID("Android") + } + + + override fun toString(): String { + return Gson().toJson(this).toString() + } + } + + override fun toString(): String { + return Gson().toJson(this).toString() + } +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/RegistrationApi.kt b/android/app/src/main/java/com/cumulocity/alarmapp/util/RegistrationApi.kt new file mode 100644 index 0000000..6063305 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/RegistrationApi.kt @@ -0,0 +1,80 @@ +// Copyright (c) 2014-2023 Software AG, Darmstadt, Germany and/or Software AG USA Inc., Reston, VA, USA, and/or its subsidiaries and/or its affiliates and/or their licensors. +// Use, reproduction, transfer, publication or disclosure is prohibited except as specifically provided for in your License Agreement with Software AG. + +package com.cumulocity.client.api + +import com.cumulocity.client.model.Registration +import okhttp3.OkHttpClient +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.converter.scalars.ScalarsConverterFactory +import retrofit2.http.* + +/** + * Endpoint that is used to register physical devices at the tenant. + */ +interface RegistrationApi { + + companion object Factory { + fun create(baseUrl: String): RegistrationApi { + return create(baseUrl, null) + } + + fun create(baseUrl: String, clientBuilder: OkHttpClient.Builder?): RegistrationApi { + val retrofitBuilder = retrofit().baseUrl(baseUrl) + if (clientBuilder != null) { + retrofitBuilder.client(clientBuilder.build()) + } + return retrofitBuilder.build().create(RegistrationApi::class.java) + } + + fun retrofit(): Retrofit.Builder { + return Retrofit.Builder() + .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(ScalarsConverterFactory.create()) + } + } + + /** + * Registers a device at the tenant. + * + * "Creates a Azure registration within the configured notification hub. Needs to be performed once the device token is obtained from APNS or Firebase.Use this resource to also update an existing registration. E.g. to modify the tag list." + * + * ##### Response Codes + * + * The following table gives an overview of the possible response codes and their meanings: + * + * * HTTP 201 Successfully registered device. + * * HTTP 400 Could not create Azure registration. + * * HTTP 401 Authentication information is missing or invalid. + * + * @param body + */ + @Headers(*["Content-Type:application/json", "Accept:application/json"]) + @POST("/service/pushgateway/registrations") + fun subscribe( + @Body body: Registration + ): Call + + /** + * Removes a registration given by it's device token. + * + * ##### Response Codes + * + * The following table gives an overview of the possible response codes and their meanings: + * + * * HTTP 201 The registration has been removed. + * * HTTP 400 The registration could not been removed. + * * HTTP 401 Authentication information is missing or invalid. + * + * @param deviceToken + * A registered device token. + */ + @Headers("Accept:application/json") + @DELETE("/service/pushgateway/registrations/{deviceToken}") + fun unsubscribe( + @Path("deviceToken") deviceToken: String + ): Call +} diff --git a/android/app/src/main/java/com/cumulocity/alarmapp/util/StringUtil.java b/android/app/src/main/java/com/cumulocity/alarmapp/util/StringUtil.java new file mode 100644 index 0000000..df0a118 --- /dev/null +++ b/android/app/src/main/java/com/cumulocity/alarmapp/util/StringUtil.java @@ -0,0 +1,13 @@ +package com.cumulocity.alarmapp.util; + +public class StringUtil { + public static String toCamelCase(String text) { + if (text != null && !text.isEmpty()) { + StringBuilder builder = new StringBuilder(); + builder.append(text.substring(0, 1).toUpperCase()); + builder.append(text.substring(1).toLowerCase()); + return builder.toString(); + } + return text; + } +} diff --git a/android/app/src/main/res/anim/slide_in_right.xml b/android/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..7c490ac --- /dev/null +++ b/android/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/anim/slide_out_left.xml b/android/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..20376e5 --- /dev/null +++ b/android/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_baseline_add.xml b/android/app/src/main/res/drawable/ic_baseline_add.xml new file mode 100644 index 0000000..5f95e49 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_baseline_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_comment_count.xml b/android/app/src/main/res/drawable/ic_comment_count.xml new file mode 100644 index 0000000..328d0fc --- /dev/null +++ b/android/app/src/main/res/drawable/ic_comment_count.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_empty_alarms.xml b/android/app/src/main/res/drawable/ic_empty_alarms.xml new file mode 100644 index 0000000..2740946 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_empty_alarms.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_empty_comment.xml b/android/app/src/main/res/drawable/ic_empty_comment.xml new file mode 100644 index 0000000..ee5d915 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_empty_comment.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_filter.xml b/android/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000..b765348 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..8a19b7b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..3b343cd --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_severity_critical.xml b/android/app/src/main/res/drawable/ic_severity_critical.xml new file mode 100644 index 0000000..a3d7d60 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_severity_critical.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_severity_major.xml b/android/app/src/main/res/drawable/ic_severity_major.xml new file mode 100644 index 0000000..d6dde73 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_severity_major.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_severity_minor.xml b/android/app/src/main/res/drawable/ic_severity_minor.xml new file mode 100644 index 0000000..d60dd77 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_severity_minor.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_severity_warning.xml b/android/app/src/main/res/drawable/ic_severity_warning.xml new file mode 100644 index 0000000..bb4b1b2 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_severity_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_status_acknowledged.xml b/android/app/src/main/res/drawable/ic_status_acknowledged.xml new file mode 100644 index 0000000..74da446 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_status_acknowledged.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_status_active.xml b/android/app/src/main/res/drawable/ic_status_active.xml new file mode 100644 index 0000000..997233c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_status_active.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_status_cleared.xml b/android/app/src/main/res/drawable/ic_status_cleared.xml new file mode 100644 index 0000000..dfe87d1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_status_cleared.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/sag_logo.xml b/android/app/src/main/res/drawable/sag_logo.xml new file mode 100644 index 0000000..82f556d --- /dev/null +++ b/android/app/src/main/res/drawable/sag_logo.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/splash_background.xml b/android/app/src/main/res/drawable/splash_background.xml new file mode 100644 index 0000000..5f95ecf --- /dev/null +++ b/android/app/src/main/res/drawable/splash_background.xml @@ -0,0 +1,841 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_login.xml b/android/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..e3c8599 --- /dev/null +++ b/android/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_splash_screen.xml b/android/app/src/main/res/layout/activity_splash_screen.xml new file mode 100644 index 0000000..386ac3f --- /dev/null +++ b/android/app/src/main/res/layout/activity_splash_screen.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/alarm_list_header.xml b/android/app/src/main/res/layout/alarm_list_header.xml new file mode 100644 index 0000000..4f0edbd --- /dev/null +++ b/android/app/src/main/res/layout/alarm_list_header.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/alarm_list_item.xml b/android/app/src/main/res/layout/alarm_list_item.xml new file mode 100644 index 0000000..f56753c --- /dev/null +++ b/android/app/src/main/res/layout/alarm_list_item.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/alarm_text_template.xml b/android/app/src/main/res/layout/alarm_text_template.xml new file mode 100644 index 0000000..266c203 --- /dev/null +++ b/android/app/src/main/res/layout/alarm_text_template.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/bottomsheet_alarmfilter_layout.xml b/android/app/src/main/res/layout/bottomsheet_alarmfilter_layout.xml new file mode 100644 index 0000000..78a5e18 --- /dev/null +++ b/android/app/src/main/res/layout/bottomsheet_alarmfilter_layout.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/comment_list_item.xml b/android/app/src/main/res/layout/comment_list_item.xml new file mode 100644 index 0000000..bfb2256 --- /dev/null +++ b/android/app/src/main/res/layout/comment_list_item.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/empty_comment_list.xml b/android/app/src/main/res/layout/empty_comment_list.xml new file mode 100644 index 0000000..f0b3d4a --- /dev/null +++ b/android/app/src/main/res/layout/empty_comment_list.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/empty_list.xml b/android/app/src/main/res/layout/empty_list.xml new file mode 100644 index 0000000..e8bbc7d --- /dev/null +++ b/android/app/src/main/res/layout/empty_list.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_add_comment.xml b/android/app/src/main/res/layout/fragment_add_comment.xml new file mode 100644 index 0000000..e860210 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_add_comment.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_alarm_details.xml b/android/app/src/main/res/layout/fragment_alarm_details.xml new file mode 100644 index 0000000..873c716 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_alarm_details.xml @@ -0,0 +1,331 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +