diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml new file mode 100644 index 0000000..4e398ed --- /dev/null +++ b/.idea/assetWizardSettings.xml @@ -0,0 +1,32 @@ + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/eric.xml b/.idea/dictionaries/eric.xml new file mode 100644 index 0000000..e207c24 --- /dev/null +++ b/.idea/dictionaries/eric.xml @@ -0,0 +1,7 @@ + + + + acmi + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 51fa3e5..d04819c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,7 +5,7 @@ diff --git a/README.md b/README.md index 7ccf70a..c876f22 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,23 @@ View when the room is reserved: +# Companion Mode +-BetterVent now supports a Companion Mode; rather than just viewing the app as a kiosk, users can download the application and configure it to show locations throughout their calendar. + + + +-Choose from automatically discovered locations which ones to track + + + + + +-Get detailed information on a room's status with a simple click + + + + + # How do I get it? Currently, BetterVent is not on the Play Store, but you can download the .apk file in the releases tab. (I'll try to keep it up to date) @@ -43,7 +60,7 @@ Currently, BetterVent is not on the Play Store, but you can download the .apk fi - Better parsing of event keywords - Set keywords that usually pertain to a location - Colors - + ## Device Admin To set the app as device admin (You need to do this before kiosk features work (Thanks, Google)) Connect to a computer and in the terminal (after installing adb) do this *BEFORE SETTING UP A GOOGLE ACCOUNT*: diff --git a/app/build.gradle b/app/build.gradle index e55aba0..6d2f5e1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,16 +1,17 @@ apply plugin: 'com.android.application' -apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { applicationId "edu.rit.csh.bettervent" - minSdkVersion 23 - targetSdkVersion 28 + minSdkVersion 26 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true multiDexEnabled true } @@ -22,27 +23,34 @@ android { } } + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:design:28.0.0' - implementation 'com.android.support:support-vector-drawable:28.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation 'org.jetbrains.anko:anko-commons:0.10.8' implementation 'org.jetbrains.anko:anko-design:0.10.8' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' // Google Calendar API dependencies - implementation 'com.google.android.gms:play-services:12.0.1' - api 'com.google.apis:google-api-services-calendar:v3-rev119-1.19.1' - api 'com.google.api-client:google-api-client:1.23.0' - api 'com.google.api-client:google-api-client-android:1.23.0' - api 'com.google.api-client:google-api-client-gson:1.19.1' - + implementation 'com.google.api-client:google-api-client:1.23.0' + implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' + implementation 'com.google.apis:google-api-services-calendar:v3-rev305-1.23.0' + implementation 'com.google.android.gms:play-services-base:17.1.0' implementation 'com.github.thellmund:Android-Week-View:3.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.google.android.gms:play-services-auth:17.0.0' + implementation 'com.google.api-client:google-api-client:1.23.0' + implementation 'com.google.api-client:google-api-client-android:1.23.0' + implementation 'com.google.apis:google-api-services-people:v1-rev4-1.22.0' + implementation 'com.google.http-client:google-http-client-gson:1.19.0' } configurations { @@ -52,4 +60,7 @@ configurations { } repositories { mavenCentral() -} \ No newline at end of file +} +androidExtensions { + experimental = true +} diff --git a/app/src/androidTest/java/edu/rit/csh/bettervent/ExampleInstrumentedTest.java b/app/src/androidTest/java/edu/rit/csh/bettervent/ExampleInstrumentedTest.java index 13a8454..dbada70 100644 --- a/app/src/androidTest/java/edu/rit/csh/bettervent/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/edu/rit/csh/bettervent/ExampleInstrumentedTest.java @@ -1,8 +1,9 @@ package edu.rit.csh.bettervent; import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.runner.AndroidJUnit4; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4fb2e6f..e71dcbb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,14 @@ + - - @@ -20,23 +21,34 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Design.Light.NoActionBar"> - + + + - + + - + diff --git a/app/src/main/java/edu/rit/csh/bettervent/ApiAsyncTask.kt b/app/src/main/java/edu/rit/csh/bettervent/ApiAsyncTask.kt deleted file mode 100644 index b600073..0000000 --- a/app/src/main/java/edu/rit/csh/bettervent/ApiAsyncTask.kt +++ /dev/null @@ -1,80 +0,0 @@ -package edu.rit.csh.bettervent - -import android.content.Context -import android.content.SharedPreferences -import android.os.AsyncTask -import com.google.api.client.googleapis.extensions.android.gms.auth.GooglePlayServicesAvailabilityIOException -import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException -import com.google.api.client.util.DateTime - -import com.google.api.services.calendar.model.* - -import java.io.IOException - -/** - * An asynchronous task that handles the Google Calendar API call. - * Placing the API calls in their own task ensures the UI stays responsive. - */ - -/** - * Created by miguel on 5/29/15. - */ - -class ApiAsyncTask -/** - * Constructor. - * @param activity MainActivity that spawned this task. - */ -internal constructor(private val mainActivity: MainActivity) : AsyncTask() { - - private val appSettings: SharedPreferences? = null - - /** - * Background task to call Google Calendar API. - * @param params no parameters needed for this task. - */ - override fun doInBackground(vararg params: Void): Void? { - try { - mainActivity.clearResultsText() - mainActivity.updateResultsText(getDataFromApi(mainActivity.calendarID!!, mainActivity.maxResults)) - - } catch (availabilityException: GooglePlayServicesAvailabilityIOException) { - // mainActivity.showGooglePlayServicesAvailabilityErrorDialog( - // availabilityException.getConnectionStatusCode()); //TODO: Display error when unable to fetch events. - System.err.println("Error connecting to Google Play Services. Error code: " + availabilityException.connectionStatusCode) - - - } catch (userRecoverableException: UserRecoverableAuthIOException) { - mainActivity.startActivityForResult( - userRecoverableException.intent, - MainActivity.REQUEST_AUTHORIZATION) - - } catch (e: IOException) { - mainActivity.updateStatus("The following error occurred: " + e.message) - } - - return null - } - - /** - * Fetch a list of the next 10 events from the primary calendar. - * @return List of Strings describing returned events. - * @throws IOException - */ - @Throws(IOException::class) - private fun getDataFromApi(calendarID: String, maxResults: Int): List { - // Load up app settings to fetch passwords and background colors. - // System.out.println("*** Attempting to get data from API. ***"); - // List the next 10 events from the primary calendar. - val now = DateTime(System.currentTimeMillis()) - val events = mainActivity.mService.events().list(calendarID) - .setMaxResults(maxResults) - .setTimeMin(now) - .setOrderBy("startTime") - .setSingleEvents(true) - .execute() -// println("*** items: " + events) - return events.items - } - -} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/MainActivity.kt b/app/src/main/java/edu/rit/csh/bettervent/MainActivity.kt deleted file mode 100644 index 1616bfc..0000000 --- a/app/src/main/java/edu/rit/csh/bettervent/MainActivity.kt +++ /dev/null @@ -1,447 +0,0 @@ -package edu.rit.csh.bettervent - -import android.accounts.Account -import android.app.admin.DevicePolicyManager -import android.content.ComponentName -import android.support.design.widget.BottomNavigationView -import android.support.design.widget.FloatingActionButton -import android.support.v4.app.Fragment -import android.support.v7.app.AppCompatActivity -import android.os.Bundle -import android.view.MenuItem - -import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential - -import android.accounts.AccountManager -import android.app.Activity -import android.app.Dialog -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.ConnectivityManager -import android.net.NetworkInfo -import android.os.Handler -import android.view.View -import android.widget.TextClock -import android.widget.Toast - -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GooglePlayServicesUtil -import com.google.api.client.extensions.android.http.AndroidHttp -import com.google.api.client.http.HttpTransport -import com.google.api.client.json.JsonFactory -import com.google.api.client.json.gson.GsonFactory -import com.google.api.client.util.DateTime -import com.google.api.client.util.ExponentialBackOff -import com.google.api.services.calendar.CalendarScopes -import com.google.api.services.calendar.model.* - -import java.lang.reflect.Array -import java.util.ArrayList -import java.util.Arrays - -class MainActivity : AppCompatActivity() { - - internal lateinit var mService: com.google.api.services.calendar.Calendar - - internal lateinit var credential: GoogleAccountCredential - // This MainActivity gets the data from the API, and holds it - // as a list. The Fragments then update themselves using that. - private val APIOutList = ArrayList() - internal val transport = AndroidHttp.newCompatibleTransport() - internal val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance() - var calendarID: String? = null - var maxResults: Int = 0 - - private var mAppSettings: SharedPreferences? = null - - private lateinit var APIStatusMessage: String - var isReserved = true - private lateinit var bottomNav: BottomNavigationView - private lateinit var refreshButton: FloatingActionButton - - private val navListener = BottomNavigationView.OnNavigationItemSelectedListener { item -> - selectedFragment = null - when (item.itemId) { - R.id.navigation_status -> - // selectedFragment = new StatusFragment(); - selectedFragment = StatusFragment.newInstance(APIOutList) - R.id.navigation_schedule -> selectedFragment = ScheduleFragment.newInstance(APIOutList) - R.id.navigation_quick_mode -> selectedFragment = QuickModeFragment() - } - - // System.out.println("*** currentEventTitle: " + currentEventTitle); - supportFragmentManager.beginTransaction().replace(R.id.fragment_container, - selectedFragment!!).commit() - - true - } - - /** - * Checks whether the device currently has a network connection. - * @return true if the device has a network connection, false otherwise. - */ - private val isDeviceOnline: Boolean - get() { - val connMgr = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val networkInfo = connMgr.activeNetworkInfo - return networkInfo != null && networkInfo.isConnected - } - - /** - * Check that Google Play services APK is installed and up to date. Will - * launch an error dialog for the user to update Google Play Services if - * possible. - * @return true if Google Play Services is available and up to - * date on this device; false otherwise. - */ - private val isGooglePlayServicesAvailable: Boolean - get() { - val connectionStatusCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) - if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) { - showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode) - return false - } else if (connectionStatusCode != ConnectionResult.SUCCESS) { - return false - } - return true - } - - - /** - * Checks the times of the first event in APIOutList (the List of Events generated by the API) - * and if the current time is within those times, then the room is booked - * and if the current time is not within those times, the room is free. - * @return: true if the current time is outside of the time of the - * next event, and false if vice-versa. - */ - private// Then the room is currently in use. - // If something weird happens, just assume the room is free. - val isFree: Boolean - get() { - try { - val now = DateTime(System.currentTimeMillis()) - val firstEventStart = APIOutList[0].start.dateTime - val firstEventEnd = APIOutList[0].end.dateTime - if (now.value > firstEventStart.value && now.value < firstEventEnd.value) { - isReserved = true - return false - } else { - isReserved = false - return true - } - } catch (e: Exception) { - isReserved = false - return true - } - - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - - // Load up app settings to fetch passwords and background colors. - mAppSettings = getSharedPreferences( - getString(R.string.preference_file_key), Context.MODE_PRIVATE) - - // Must restart for these preferences to take hold. - calendarID = mAppSettings!!.getString("edu.rit.csh.bettervent.calendarid", "") - val maxResultsStr = mAppSettings!!.getString("edu.rit.csh.bettervent.maxresults", "") - if (maxResultsStr !== "" && maxResultsStr != null) - maxResults = Integer.parseInt(maxResultsStr) - else { - infoPrint("Max Results not set. Defaulting to 100.") - maxResults = 100 - } - - - //Following code allow the app packages to lock task in true kiosk mode - // get policy manager - val myDevicePolicyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - // get this app package name - val mDPM = ComponentName(this, AdminReceiver::class.java) - - if (myDevicePolicyManager.isDeviceOwnerApp(this.packageName)) { - // get this app package name - val packages = arrayOf(this.packageName) - // mDPM is the admin package, and allow the specified packages to lock task - myDevicePolicyManager.setLockTaskPackages(mDPM, packages) - } else { - Toast.makeText(applicationContext, "Not owner", Toast.LENGTH_LONG).show() - } - - startLockTask() - - - bottomNav = findViewById(R.id.bottom_navigation) - bottomNav.setOnNavigationItemSelectedListener(navListener) - - refreshButton = findViewById(R.id.refresh_button) - - refreshButton.setOnClickListener { - // TODO: figure out why you have to do this twice to make anything happen. - refreshResults() - refreshUI() - } - - // Initialize credentials and service object. - val settings = getPreferences(Context.MODE_PRIVATE) - credential = GoogleAccountCredential.usingOAuth2( - applicationContext, Arrays.asList(*SCOPES)) - .setBackOff(ExponentialBackOff()) - .setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null)) - - mService = com.google.api.services.calendar.Calendar.Builder( - transport, jsonFactory, credential) - .setApplicationName("Google Calendar API Android Quickstart") - .build() - - refreshResults() - if (selectedFragment == null) { - selectedFragment = StatusFragment.newInstance(APIOutList) - supportFragmentManager.beginTransaction().replace(R.id.fragment_container, - selectedFragment!!).commit() - } - - centralClock = findViewById(R.id.central_clock) - - // Initialize API Refresher. Make sure to sign into a google account before launching the app. - val handler = Handler() - val runnable = object : Runnable { - override fun run() { - if (credential.selectedAccountName != null) { - refreshResults() - println(" *** Refreshed.") - refreshUI() - handler.postDelayed(this, 10000) - } - } - } - - //Start API Refresher - handler.postDelayed(runnable, 1000) - - } - - /** - * Called whenever this activity is pushed to the foreground, such as after - * a call to onCreate(). - */ - override fun onResume() { - super.onResume() - if (isGooglePlayServicesAvailable) { - refreshResults() - } else { - APIStatusMessage = "Google Play Services required: " + "after installing, close and relaunch this app." - } - } - - /** - * Called when an activity launched here (specifically, AccountPicker - * and authorization) exits, giving you the requestCode you started it with, - * the resultCode it returned, and any additional data from it. - * @param requestCode code indicating which activity result is incoming. - * @param resultCode code indicating the result of the incoming - * activity result. - * @param data Intent (containing result data) returned by incoming - * activity result. - */ - override fun onActivityResult( - requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - infoPrint("API Request code returned: $requestCode") - when (requestCode) { - REQUEST_GOOGLE_PLAY_SERVICES -> if (resultCode == Activity.RESULT_OK) { - refreshResults() - } else { - isGooglePlayServicesAvailable - } - REQUEST_ACCOUNT_PICKER -> { - infoPrint("Pick your account.") - if (resultCode == Activity.RESULT_OK && data != null && - data.extras != null) { - infoPrint("Result = $resultCode") - val accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) - infoPrint("Account name = " + accountName!!) - if (accountName != null) { - // credential.setSelectedAccountName(accountName); - credential.selectedAccount = Account(accountName, "edu.rit.csh.bettervent") - infoPrint("Account name set. Account name = $accountName") - infoPrint(credential.selectedAccountName) - val settings = getPreferences(Context.MODE_PRIVATE) - val editor = settings.edit() - editor.putString(PREF_ACCOUNT_NAME, accountName) - editor.commit() - refreshResults() - } - } else if (resultCode == Activity.RESULT_CANCELED) { - infoPrint("Account Unspecified") - APIStatusMessage = "Account unspecified." - } - } - REQUEST_AUTHORIZATION -> if (resultCode == Activity.RESULT_OK) { - if (credential.selectedAccountName.length < 1) - refreshResults() - } else { - chooseAccount() - } - } - - super.onActivityResult(requestCode, resultCode, data) - } - - /** - * Attempt to get a set of data from the Google Calendar API to display. If the - * email address isn't known yet, then call chooseAccount() method so the - * user can pick an account. - */ - private fun refreshResults() { - println("*** Refreshing results... ***") - if (credential.selectedAccountName == null) { - infoPrint("No account selected.") - chooseAccount() - } else { - if (isDeviceOnline) { - println("*** Executing APIAsyncTask. ***") - ApiAsyncTask(this).execute() - // TODO: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState ??? - } else { - println("*** Can't refresh calendar. ***") - APIStatusMessage = "No network connection available." - } - } - } - - fun refreshUI() { - try { - when (selectedFragment!!.javaClass){ - StatusFragment::class.java -> { - selectedFragment = StatusFragment.newInstance(APIOutList) - supportFragmentManager.beginTransaction().replace(R.id.fragment_container, - selectedFragment!!).commit() - println(" *** Refreshed Status UI") - } - ScheduleFragment::class.java -> { - selectedFragment = ScheduleFragment.newInstance(APIOutList) - supportFragmentManager.beginTransaction().replace(R.id.fragment_container, - selectedFragment!!).commit() - println(" *** Refreshed Schedule UI") - } - else -> println(" *** UI is not status.") - } - } catch (e: Exception) { - System.err.println("Caught Exception\n$e") - } - - } - - /** - * Clear any existing Google Calendar API data from the TextView and update - * the header message; called from background threads and async tasks - * that need to update the UI (in the UI thread). - */ - fun clearResultsText() { - runOnUiThread { - APIStatusMessage = "Retrieving data…" - APIStatusMessage = "" - } - } - - /** - * Fill the data TextView with the given List of Strings; called from - * background threads and async tasks that need to update the UI (in the - * UI thread). - * @param dataEvents a List of Strings to populate the main TextView with. - */ - fun updateResultsText(dataEvents: List?) { - runOnUiThread { - if (dataEvents == null) { - APIStatusMessage = "Error retrieving data!" - } else if (dataEvents.size == 0) { - APIStatusMessage = "No data found." - println("*** No data found. ***") - APIOutList.removeAll(APIOutList) - } else { - APIStatusMessage = "API Call Complete." - infoPrint("*** Events found. *** $dataEvents") - val eventKeyword = mAppSettings!!.getString("edu.rit.csh.bettervent.filterkeywords", "") - APIOutList.removeAll(APIOutList) - var eventFieldToCheck: String? - for (event in dataEvents) { - if (mAppSettings!!.getBoolean("edu.rit.csh.bettervent.filterbytitle", false)) { - eventFieldToCheck = event.summary - } else { - eventFieldToCheck = event.location - } - infoPrint(eventFieldToCheck) - if (eventKeyword!!.length > 0 && eventFieldToCheck != null) { - if (eventFieldToCheck.toLowerCase().contains(eventKeyword.toLowerCase())) { - APIOutList.add(event) - } - } else if (eventKeyword.length < 1) { - APIOutList.add(event) - } - } - isFree - } - } - } - - /** - * Show a status message in the list header TextView; called from background - * threads and async tasks that need to update the UI (in the UI thread). - * @param message a String to display in the UI header TextView. - */ - fun updateStatus(message: String) { - runOnUiThread { APIStatusMessage = message } - } - - /** - * Starts an activity in Google Play Services so the user can pick an - * account. - */ - private fun chooseAccount() { - startActivityForResult( - credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER) - } - - /** - * Display an error dialog showing that Google Play Services is missing - * or out of date. - * @param connectionStatusCode code describing the presence (or lack of) - * Google Play Services on this device. - */ - internal fun showGooglePlayServicesAvailabilityErrorDialog( - connectionStatusCode: Int) { - runOnUiThread { - val dialog = GooglePlayServicesUtil.getErrorDialog( - connectionStatusCode, - this@MainActivity, - REQUEST_GOOGLE_PLAY_SERVICES) - dialog.show() - } - } - - private fun infoPrint(info: Any?) { - if (info != null) - println("MAIN_: " + info!!) - else println("MAIN_: null!?!?!?") - } - - companion object { - - internal val REQUEST_ACCOUNT_PICKER = 1000 - internal val REQUEST_AUTHORIZATION = 1001 - internal val REQUEST_GOOGLE_PLAY_SERVICES = 1002 - private val PREF_ACCOUNT_NAME = "accountName" - private val SCOPES = arrayOf(CalendarScopes.CALENDAR_READONLY) - - lateinit var centralClock: TextClock - - //UI Elements visible throughout the app - var selectedFragment: Fragment? = null - } - -} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/StatusFragment.kt b/app/src/main/java/edu/rit/csh/bettervent/StatusFragment.kt deleted file mode 100644 index 2114c86..0000000 --- a/app/src/main/java/edu/rit/csh/bettervent/StatusFragment.kt +++ /dev/null @@ -1,273 +0,0 @@ -package edu.rit.csh.bettervent - -import android.app.AlertDialog -import android.content.Context -import android.content.SharedPreferences -import android.os.Bundle -import android.support.v4.app.Fragment -import android.text.InputType -import android.text.method.PasswordTransformationMethod -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.RelativeLayout - -import com.google.api.client.util.DateTime -import com.google.api.services.calendar.model.Event -import kotlinx.android.synthetic.main.fragment_status.* -import kotlinx.android.synthetic.main.fragment_status.view.* -import kotlinx.android.synthetic.main.password_alert.* -import kotlinx.android.synthetic.main.password_alert.view.* -import org.jetbrains.anko.alert -import org.jetbrains.anko.noButton -import org.jetbrains.anko.yesButton - -import java.io.Serializable - -class StatusFragment : Fragment() { - - private lateinit var appSettings: SharedPreferences // Settings object containing user preferences. - - var events: ArrayList = ArrayList() - - // Variables for storing what the status should read out as - var currentTitle: String? = null - lateinit var currentTime: String - var nextTitle: String? = null - lateinit var nextTime: String - - /** - * Extract information from the bundle that may have been provided with the StatusFragment, - * inflate status_layout and set it as the currently active view, then make references to all of - * the various pieces of the UI so that the class can update the UI with the API data. - * - * @param inflater - * @param container - * @param savedInstanceState - * @return - */ - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - infoPrint("Loaded Status Fragment.") - - // Load up app settings to fetch passwords and background colors. - appSettings = context!!.getSharedPreferences( - getString(R.string.preference_file_key), Context.MODE_PRIVATE) - - val args = arguments - if (args != null) { - events.addAll(args.getSerializable("events") as List) - getCurrentAndNextEvents() - - } else { - infoPrint("ERROR! NO DATA FOUND!") - } - - val view = inflater.inflate(R.layout.fragment_status, container, false) - - MainActivity.centralClock.setTextColor(-0x1) - - fun showAlertWithFunction(onSuccess: () -> Unit){ - context!!.alert("Enter Password:"){ - val v = layoutInflater.inflate(R.layout.password_alert, null) - customView = v - fun checkPassword(pw: String){ - if (pw == appSettings!!.getString("edu.rit.csh.bettervent.password", "")) onSuccess() - } - yesButton { checkPassword(v.password_et.text.toString()) } - noButton { dialog -> dialog.cancel() } - }.show() - } - - view.leave_button.setOnClickListener { - showAlertWithFunction { System.exit(0) } - } - - view.settings_button.setOnClickListener { - MainActivity.selectedFragment = SettingsFragment() - - showAlertWithFunction { fragmentManager!!.beginTransaction().replace(R.id.fragment_container, - MainActivity.selectedFragment as SettingsFragment).commit() } - } - - if (currentTitle == null) { - nextTime = "" - nextTitle = nextTime - currentTime = nextTitle as String - currentTitle = currentTime - } - if (nextTitle == null) nextTitle = "" - return view - } - - /** - * @param view - * @param savedInstanceState - */ - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - infoPrint("Fragment Event Title: " + currentTitle!!) - setRoomStatus() - } - - /** - * - */ - private fun setRoomStatus() { - // Set current status of the room - if (currentTitle != "") { - free_label.visibility = View.INVISIBLE - reserved_label.visibility = View.VISIBLE - event_title.text = currentTitle - event_time!!.text = currentTime - status_layout.setBackgroundColor(resources.getColor(R.color.CSHRed)) - } else { - next_event_title.textSize = 64F - next_event_time.textSize = 32F - - reserved_label.visibility = View.INVISIBLE - free_label.visibility = View.VISIBLE - event_title.text = "" - event_time.text = "" - event_title.visibility = View.GONE - event_time.visibility = View.GONE - status_layout.setBackgroundColor(resources.getColor(R.color.CSHGreen)) - } - - if (currentTitle == "") { -// val next_label_params = next_label.layoutParams as RelativeLayout.LayoutParams -// next_label_params.setMargins(0, 128, 0, 0) //substitute parameters for left, top, right, bottom -// next_event_title.layoutParams = next_label_params - - val next_event_title_params = next_event_title.layoutParams as RelativeLayout.LayoutParams - next_event_title_params.setMargins(0, 72, 0, 0) //substitute parameters for left, top, right, bottom - next_event_title.layoutParams = next_event_title_params - - val separator_params = separator.layoutParams as RelativeLayout.LayoutParams - separator_params.setMargins(0, 0, 0, 0) //substitute parameters for left, top, right, bottom - separator.layoutParams = separator_params - - - } - - // Set the future status of the room - if (nextTitle != "") { - next_label.visibility = View.VISIBLE - next_event_title.text = nextTitle - next_event_time.text = nextTime - } else { - val next_event_title_params = next_event_title.layoutParams as RelativeLayout.LayoutParams - next_event_title_params.setMargins(0, 192, 0, 0) //substitute parameters for left, top, right, bottom - next_event_title.layoutParams = next_event_title_params - - next_label.visibility = View.GONE - next_event_time.visibility = View.GONE - next_event_title.text = "There are no upcoming events." - next_event_time.text = "" - } - } - - /** - * Looks at the APIOutList (the List of Events generated by the API), - * and based on how many there are and when they are, sets the string - * values for currentEventTitle, currentEventTime, nextEventTitle, and - * nextEventTime. - */ - private fun getCurrentAndNextEvents() { - if (events == null) - infoPrint("There may have been an issue getting the data." + "\nor maybe there was no data.") - - if (events == null || events!!.isEmpty()) { - nextTime = "" - nextTitle = nextTime - currentTime = nextTitle as String - currentTitle = currentTime - } else { - //Here's all the data we'll need. - val summary = events!![0].summary - var start: DateTime? = events!![0].start.dateTime - val end = events!![0].end.dateTime - - if (start == null) { - // If the event will last all day then only use the event title. - start = events!![0].start.date - currentTitle = summary - currentTime = "All day" - } else { - // If the event has a set start and end time then check if it's now or later. - val now = DateTime(System.currentTimeMillis()) - if (start.value > now.value) { - // If the first event will happen in the future - // Then there is no current event. - currentTitle = "" - currentTime = "" - nextTitle = summary - nextTime = formatDateTime(start) + " — " + formatDateTime(end) - } else { - // Set current event to first event if it's happening right now. - currentTitle = summary - currentTime = formatDateTime(start) + " — " + formatDateTime(end) - if (events!!.size > 1) - // Get the next event after this one - getNextEvent() - } - } - } - } - - /** - * Takes the second index of APIOutList (the List of Events generated by the API) - * and sets nextEventTitle and nextEventTime. - */ - private fun getNextEvent() { - try { - val nextEventSummary = events!![1].summary - var nextEventStart: DateTime? = events!![1].start.dateTime - val nextEventEnd = events!![1].end.dateTime - if (nextEventStart == null) { - // All-day events don't have start times, so just use - // the start date. - nextEventStart = events!![1].start.date - } - nextTitle = nextEventSummary - nextTime = formatDateTime(nextEventStart!!) + " — " + formatDateTime(nextEventEnd) - } catch (e: Exception) { - nextTitle = "" - nextTime = "" - } - } - - /** - * Method to format DateTimes into human-readable strings - * - * @param dateTime: DateTime to make readable - * @return: HH:MM on YYYY/MM/DD - */ - private fun formatDateTime(dateTime: DateTime): String { - return if (dateTime.isDateOnly) { - dateTime.toString() - } else { - val t = dateTime.toString().split("T".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val time = t[1].substring(0, 5) - val date = t[0].split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() - val dateString = date[0] + "/" + date[1] + "/" + date[2] - - "$time on $dateString" - } - } - - private fun infoPrint(info: String) { - println("STAT_: $info") - } - - companion object { - - fun newInstance(events: List): StatusFragment { - val f = StatusFragment() - val args = Bundle() - args.putSerializable("events", events as Serializable) - f.arguments = args - return f - } - } -} diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/CalendarInfoAdapter.kt b/app/src/main/java/edu/rit/csh/bettervent/view/CalendarInfoAdapter.kt new file mode 100644 index 0000000..9eb74e6 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/CalendarInfoAdapter.kt @@ -0,0 +1,27 @@ +package edu.rit.csh.bettervent.view + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import edu.rit.csh.bettervent.R +import kotlinx.android.synthetic.main.add_location_item.view.* + +class CalendarInfoAdapter(val context: Context, private val items: List, val onItemSelected: (CalendarInfo) -> Unit): RecyclerView.Adapter() { + + override fun getItemCount() = items.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.itemView.add_location_tv.text = items[position].name + holder.itemView.setOnClickListener { onItemSelected.invoke(items[position]) } + if (position == itemCount - 1) holder.itemView.divider.visibility = View.GONE + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(context) + return ViewHolder(inflater.inflate(R.layout.add_location_item, parent, false)) + } + + inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) +} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/MainActivity.kt b/app/src/main/java/edu/rit/csh/bettervent/view/MainActivity.kt new file mode 100644 index 0000000..1a88905 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/MainActivity.kt @@ -0,0 +1,276 @@ +package edu.rit.csh.bettervent.view + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GooglePlayServicesUtil +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Scope +import com.google.android.gms.tasks.Task +import com.google.api.services.calendar.CalendarScopes +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.companion.CompanionActivity +import edu.rit.csh.bettervent.view.kiosk.MainActivity +import edu.rit.csh.bettervent.viewmodel.CompanionActivityViewModel +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.android.synthetic.main.add_location_alert.view.* +import org.jetbrains.anko.alert +import org.jetbrains.anko.okButton +import java.util.* + +class MainActivity : AppCompatActivity(){ + // This MainActivity gets the data from the API, and holds it + // as a list. The Fragments then update themselves using that. + private lateinit var mAppSettings: SharedPreferences + private lateinit var calendar: CalendarInfo + lateinit var signInClient: GoogleSignInClient + lateinit var model: CompanionActivityViewModel + + + /** + * Check that Google Play services APK is installed and up to date. Will + * launch an error dialog for the user to update Google Play Services if + * possible. + * @return true if Google Play Services is available and up to + * date on this device; false otherwise. + */ + private val isGooglePlayServicesAvailable: Boolean + get() { + val connectionStatusCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this) + if (GooglePlayServicesUtil.isUserRecoverableError(connectionStatusCode)) { + showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode) + return false + } else if (connectionStatusCode != ConnectionResult.SUCCESS) { + return false + } + return true + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // Load up app settings to fetch passwords and background colors. + mAppSettings = applicationContext.getSharedPreferences( + getString(R.string.preference_file_key), Context.MODE_PRIVATE)!! + + model = ViewModelProviders.of(this).get(CompanionActivityViewModel::class.java) + + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope("https://www.googleapis.com/auth/calendar.readonly")) + .requestEmail() + .build() + + calendar = CalendarInfo( + mAppSettings.getString("calendarName", "")!!, + mAppSettings.getString("edu.rit.csh.bettervent.calendarid", "")!! + ) + + calendar_name_tv.text = calendar.name + + signInClient = GoogleSignIn.getClient(this, gso) + + companion_btn.setOnClickListener { startCompanionActivity() } + kiosk_btn.setOnClickListener { startEventActivity() } + checkForAccount() + + choose_account_btn.setOnClickListener { signOutThenIn() } + + choose_calendar_btn.setOnClickListener { promptChooseCalendar() } + } + + private fun startCompanionActivity() { + if (calendar.name.isBlank()) { + alert{ + title = "You must select a calendar" + okButton { } + }.show() + return + } + + mAppSettings.edit() + .putString("edu.rit.csh.bettervent.calendarid", calendar.id) + .putString("calendarName", calendar.name) + .apply() + + val intent = Intent(this, CompanionActivity::class.java) + startActivity(intent) + } + + private fun startEventActivity() { + if (calendar.name.isBlank()) { + alert{ + title = "You must input a valid calendar ID" + okButton { } + }.show() + return + } + + mAppSettings.edit() + .putString("edu.rit.csh.bettervent.calendarid", calendar.id) + .putString("calendarName", calendar.name) + .apply() + + + val intent = Intent(this, MainActivity::class.java) + startActivity(intent) + } + + /** + * Called when an activity launched here (specifically, AccountPicker + * and authorization) exits, giving you the requestCode you started it with, + * the resultCode it returned, and any additional data from it. + * @param requestCode code indicating which activity result is incoming. + * @param resultCode code indicating the result of the incoming + * activity result. + * @param data Intent (containing result data) returned by incoming + * activity result. + */ + override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + infoPrint("API Request code returned: $requestCode") + when (requestCode) { + REQUEST_GOOGLE_PLAY_SERVICES -> if (resultCode != Activity.RESULT_OK) { + isGooglePlayServicesAvailable + } + RC_SIGN_IN -> { + val task = GoogleSignIn.getSignedInAccountFromIntent(data) + handleSignInResult(task) + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + private fun handleSignInResult(task: Task){ + try { + val account = task.getResult(ApiException::class.java) + mAppSettings.edit().putString(PREF_ACCOUNT_NAME, account?.email).apply() + checkForAccount() + model.refreshCalendarOptions() + } catch(e: ApiException){ + Log.w("MainActivity", "signInResult failed; code=${e.statusCode}") + } + } + + private fun signIn(){ + startActivityForResult(signInClient.signInIntent, RC_SIGN_IN) + } + + /** + * Allow the user to change accounts + */ + + private fun signOutThenIn(){ + signInClient.signOut() + .addOnCompleteListener { signIn() } + } + + /** + * Checks for a signed in account in the app; if one exists, it starts the MainActivity. + * Otherwise, it allows the user to choose an account + */ + + private fun checkForAccount(){ + val accountName = mAppSettings.getString(PREF_ACCOUNT_NAME, "")!! + account_name_tv.text = accountName + + if (accountName.isEmpty()){ + signIn() + Log.i("MainActivity", "Begin chooseAccount") + } else { + enableUI() + } + } + + /** + * Enables the UI to allow the user to choose which version they want to use, + * Kiosk or Companion + */ + private fun enableUI() { + main_root.visibility = View.VISIBLE + } + + private fun promptChooseCalendar() { + lateinit var dialog: DialogInterface + dialog = alert { + val v = layoutInflater.inflate(R.layout.add_location_alert, null) + v.add_location_rv.adapter = + CalendarInfoAdapter(applicationContext, + model.calendarItems) { item -> + dialog.dismiss() + selectCalendar(item) + } + v.add_location_rv.layoutManager = LinearLayoutManager(applicationContext) + customView = v + }.show() + } + + private fun selectCalendar(cal: CalendarInfo){ + calendar = cal + calendar_name_tv.text = cal.name + } + + /** + * Display an error dialog showing that Google Play Services is missing + * or out of date. + * @param connectionStatusCode code describing the presence (or lack of) + * Google Play Services on this device. + */ + private fun showGooglePlayServicesAvailabilityErrorDialog( + connectionStatusCode: Int) { + runOnUiThread { + val dialog = GooglePlayServicesUtil.getErrorDialog( + connectionStatusCode, + this@MainActivity, + REQUEST_GOOGLE_PLAY_SERVICES) + dialog.show() + } + } + + private fun infoPrint(info: Any) { + println("MAIN_: $info") + } + + companion object { + internal const val REQUEST_ACCOUNT_PICKER = 1000 + internal const val RC_SIGN_IN = 1001 + internal const val REQUEST_GOOGLE_PLAY_SERVICES = 1002 + private const val PREF_ACCOUNT_NAME = "accountName" + val SCOPES = arrayOf(CalendarScopes.CALENDAR_READONLY) + } +} + +data class Event(val summary: String, val start: Date, + val end: Date, val location: String) +{ + val isHappeningNow = hasStarted and !isOver + + private val isOver: Boolean + get() { + val now = Date() + return now.after(end) + } + + + private val hasStarted: Boolean + get(){ + val now = Date() + return start.before(now) + } +} + +data class CalendarInfo(var name: String, var id: String) \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/companion/CompanionActivity.kt b/app/src/main/java/edu/rit/csh/bettervent/view/companion/CompanionActivity.kt new file mode 100644 index 0000000..8c47505 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/companion/CompanionActivity.kt @@ -0,0 +1,170 @@ +package edu.rit.csh.bettervent.view.companion + +import android.content.DialogInterface +import android.graphics.Color +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.os.Parcelable +import android.view.ContextMenu +import android.view.MenuItem +import android.view.View +import android.widget.AdapterView +import android.widget.PopupMenu +import androidx.lifecycle.ViewModelProviders +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.Event +import edu.rit.csh.bettervent.viewmodel.CompanionActivityViewModel +import kotlinx.android.parcel.Parcelize +import kotlinx.android.synthetic.main.activity_companion.* +import kotlinx.android.synthetic.main.add_location_alert.view.* +import kotlinx.android.synthetic.main.location_view.view.* +import kotlinx.android.synthetic.main.fragment_status.view.event_time +import org.jetbrains.anko.alert +import java.text.SimpleDateFormat +import java.util.* + +class CompanionActivity : AppCompatActivity() { + + lateinit var viewModel: CompanionActivityViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_companion) + + locations_rv.layoutManager = LinearLayoutManager(this) + + viewModel = ViewModelProviders.of(this).get(CompanionActivityViewModel::class.java) + + refreshViewModel() + + srl.setOnRefreshListener { refreshViewModel() } + + add_fab.setOnClickListener { promptAddLocation() } + } + + private fun refreshViewModel() { + srl.isRefreshing = true + viewModel.refresh { + locations_rv.adapter = LocationCardAdapter(this, getRoomStatusesFromMap(viewModel.eventsByLocation)) { openLocationFragment(it) } + if (viewModel.usedLocations.isEmpty()) { + tooltip.visibility = View.VISIBLE + vertical_glue.visibility = View.VISIBLE + } else { + tooltip.visibility = View.GONE + vertical_glue.visibility = View.GONE + } + srl.isRefreshing = false + + registerForContextMenu(locations_rv) + } + } + + private fun getRoomStatusesFromMap(map: Map): List { + return map.entries.map { entry -> + entry.value?.let { findRoomStatus(it) } + ?: RoomStatus(entry.key, false, "No upcoming events", "", getColor(R.color.CSHGreen)) + } + } + + private fun findRoomStatus(event: Event): RoomStatus { + val timeString = formatDates(event.start, event.end) + + return if (event.isHappeningNow) { + RoomStatus(event.location, true, "Happening now: ${event.summary}", timeString, getColor(R.color.CSHRed)) + } else { + RoomStatus(event.location, false, "Upcoming: ${event.summary}", timeString, getColor(R.color.CSHGreen)) + } + } + + private fun promptAddLocation() { + lateinit var dialog: DialogInterface + dialog = alert { + val v = layoutInflater.inflate(R.layout.add_location_alert, null) + v.add_location_rv.adapter = + LocationTextAdapter(applicationContext, + viewModel.allLocations.minus(viewModel.usedLocations).toList()) { location -> + dialog.dismiss() + viewModel.addUsedLocation(location) + refreshViewModel() + } + v.add_location_rv.layoutManager = LinearLayoutManager(applicationContext) + customView = v + }.show() + } + + private fun openLocationFragment(roomStatus: RoomStatus) { + lateinit var dialog: DialogInterface + dialog = alert { + val v = layoutInflater.inflate(R.layout.location_view, null) + v.location_name.text = roomStatus.location + v.event_name.text = roomStatus.title + v.event_time.text = roomStatus.timeString + v.indicator.setBackgroundColor(roomStatus.color) + v.menu_ib.setOnClickListener { + PopupMenu(this@CompanionActivity, v.menu_ib).apply{ + setOnMenuItemClickListener { item -> + when(item.itemId) { + R.id.delete_location -> { + viewModel.removeUsedLocation(roomStatus.location) + refreshViewModel() + dialog.dismiss() + true + } + else -> false + } + } + inflate(R.menu.location_menu) + show() + } + } + + customView = v + }.show() + } +} + +fun formatDates(d1: Date, d2: Date): String { + return when { + d2.isToday() -> "${d1.formatJustTime()} - ${d2.formatJustTime()}" + d1.isToday() -> "${d1.formatJustTime()} - ${d2.formatWithDay()}" + isSameDay(d1, d2) -> "${d1.formatWithDay()} - ${d2.formatJustTime()}" + else -> "${d1.formatWithDay()} - ${d2.formatWithDay()}" + } +} + +fun isSameDay(d1: Date, d2: Date): Boolean { + + val cal1 = Calendar.getInstance() + val cal2 = Calendar.getInstance() + cal1.time = d1 + cal2.time = d2 + return cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR) && + cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR) +} + +fun Date.isToday(): Boolean { + return isSameDay(this, Date()) +} + +fun Date.formatJustTime(): String { + val simpleTimeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + return simpleTimeFormat.format(this) +} + +fun Date.formatWithDay(): String { + val simpleTimeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + val simpleDateFormat = SimpleDateFormat("MM/dd", Locale.getDefault()) + return "${simpleDateFormat.format(this)} ${simpleTimeFormat.format(this)}" +} + +data class Location(var display: String, + val keys: Set) + +@Parcelize +data class RoomStatus(val location: String, + val isBusy: Boolean, + val title: String, + val timeString: String, + val color: Int): Parcelable \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationCardAdapter.kt b/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationCardAdapter.kt new file mode 100644 index 0000000..4a13751 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationCardAdapter.kt @@ -0,0 +1,38 @@ +package edu.rit.csh.bettervent.view.companion + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import edu.rit.csh.bettervent.R +import kotlinx.android.synthetic.main.location_card.view.* + +class LocationCardAdapter(val context: Context, private val statuses: List, private val onItemClick: (RoomStatus) -> Unit): + RecyclerView.Adapter(){ + + override fun getItemCount(): Int = statuses.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val v = LayoutInflater.from(context).inflate(R.layout.location_card, parent, false) + return ViewHolder(v) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val status = statuses[position] + + holder.itemView.location_tv.text = status.location + if (status.isBusy){ + holder.itemView.card_view.setCardBackgroundColor(context.getColor(R.color.CSHRed)) + holder.itemView.title_tv.text = status.title + holder.itemView.time_tv.text = status.timeString + } else { + holder.itemView.card_view.setCardBackgroundColor(context.getColor(R.color.CSHGreen)) + holder.itemView.title_tv.text = context.getString(R.string.room_open) + } + + holder.itemView.setOnClickListener { onItemClick.invoke(status) } + } + + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) +} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationTextAdapter.kt b/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationTextAdapter.kt new file mode 100644 index 0000000..c8e0094 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/companion/LocationTextAdapter.kt @@ -0,0 +1,27 @@ +package edu.rit.csh.bettervent.view.companion + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import edu.rit.csh.bettervent.R +import kotlinx.android.synthetic.main.add_location_item.view.* + +class LocationTextAdapter(val context: Context, private val locations: List, val onLocationSelected: (String) -> Unit): RecyclerView.Adapter() { + + override fun getItemCount() = locations.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.itemView.add_location_tv.text = locations[position] + holder.itemView.setOnClickListener { onLocationSelected.invoke(locations[position]) } + if (position == itemCount - 1) holder.itemView.divider.visibility = View.GONE + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(context) + return ViewHolder(inflater.inflate(R.layout.add_location_item, parent, false)) + } + + inner class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) +} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/ParticipantListAdapter.kt b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ParticipantListAdapter.kt similarity index 96% rename from app/src/main/java/edu/rit/csh/bettervent/ParticipantListAdapter.kt rename to app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ParticipantListAdapter.kt index f1fa73f..762fd1b 100644 --- a/app/src/main/java/edu/rit/csh/bettervent/ParticipantListAdapter.kt +++ b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ParticipantListAdapter.kt @@ -1,16 +1,15 @@ -package edu.rit.csh.bettervent +package edu.rit.csh.bettervent.view.kiosk import android.app.AlertDialog import android.content.Context -import android.content.DialogInterface -import android.graphics.Typeface -import android.support.v7.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.EditText import android.widget.TextView +import edu.rit.csh.bettervent.R import java.util.ArrayList diff --git a/app/src/main/java/edu/rit/csh/bettervent/QuickModeFragment.kt b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/QuickModeFragment.kt similarity index 73% rename from app/src/main/java/edu/rit/csh/bettervent/QuickModeFragment.kt rename to app/src/main/java/edu/rit/csh/bettervent/view/kiosk/QuickModeFragment.kt index f6d2204..7dc2ef0 100644 --- a/app/src/main/java/edu/rit/csh/bettervent/QuickModeFragment.kt +++ b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/QuickModeFragment.kt @@ -1,20 +1,22 @@ -package edu.rit.csh.bettervent +package edu.rit.csh.bettervent.view.kiosk import android.app.AlertDialog -import android.content.DialogInterface import android.graphics.Typeface import android.os.Bundle -import android.support.constraint.ConstraintLayout -import android.support.v4.app.Fragment -import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.EditText +import android.widget.LinearLayout import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import edu.rit.csh.bettervent.R +import kotlinx.android.synthetic.main.fragment_quick_mode.* +import kotlinx.android.synthetic.main.fragment_quick_mode.view.* import java.util.ArrayList @@ -22,39 +24,40 @@ class QuickModeFragment : Fragment() { private val participants = ArrayList() - private var quickModeLayout: ConstraintLayout? = null + private var quickModeLayout: LinearLayout? = null - private var recyclerView: RecyclerView? = null private var adapter: RecyclerView.Adapter<*>? = null private var layoutManager: RecyclerView.LayoutManager? = null private var participantsLabel: TextView? = null private var nameSetLabel: TextView? = null - private var eventName: TextView? = null private var addButton: Button? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { infoPrint("Loaded QuickMode Fragment.") val view = inflater.inflate(R.layout.fragment_quick_mode, container, false) - MainActivity.centralClock.setTextColor(-0x1000000) - quickModeLayout = view.findViewById(R.id.quick_mode_layout) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + quickModeLayout = view.findViewById(R.id.quick_mode_view) - recyclerView = view.findViewById(R.id.participants_list) // use a linear layout manager - layoutManager = LinearLayoutManager(this.context) - recyclerView!!.layoutManager = layoutManager + layoutManager = LinearLayoutManager(context) + participants_list.layoutManager = layoutManager // specify an adapter adapter = ParticipantListAdapter(this.context!!, participants) - recyclerView!!.adapter = adapter + view.participants_list.adapter = adapter participantsLabel = view.findViewById(R.id.label_participants) nameSetLabel = view.findViewById(R.id.name_set_label) - eventName = view.findViewById(R.id.event_name) - eventName!!.setOnClickListener { + view.event_name.setOnClickListener { val builder = AlertDialog.Builder(context) builder.setTitle("Enter event title") @@ -66,16 +69,15 @@ class QuickModeFragment : Fragment() { // Set up the button builder.setPositiveButton("OK") { dialog, which -> val title = input.text.toString() - eventName!!.text = title + view.event_name.text = title //Change appearance of UI to indicate the room is reserved addButton!!.isEnabled = true quickModeLayout!!.setBackgroundColor(resources.getColor(R.color.CSHRed)) nameSetLabel!!.setTextColor(resources.getColor(R.color.white)) - eventName!!.setTextColor(resources.getColor(R.color.white)) + view.event_name.setTextColor(resources.getColor(R.color.white)) participantsLabel!!.setTextColor(resources.getColor(R.color.white)) nameSetLabel!!.visibility = View.VISIBLE - MainActivity.centralClock.setTextColor(-0x1) - eventName!!.setTypeface(null, Typeface.BOLD) + view.event_name.setTypeface(null, Typeface.BOLD) } builder.setNegativeButton("Cancel") { dialog, which -> dialog.cancel() } builder.show() @@ -101,12 +103,10 @@ class QuickModeFragment : Fragment() { adapter!!.notifyItemInserted(participants.size - 1) infoPrint("Added new person.") } - builder.setNegativeButton("Cancel") { dialog, which -> dialog.cancel() } + builder.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() } builder.show() } - - return view } fun infoPrint(info: String) { diff --git a/app/src/main/java/edu/rit/csh/bettervent/ScheduleFragment.kt b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ScheduleFragment.kt similarity index 61% rename from app/src/main/java/edu/rit/csh/bettervent/ScheduleFragment.kt rename to app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ScheduleFragment.kt index a3c7a99..d52c27c 100644 --- a/app/src/main/java/edu/rit/csh/bettervent/ScheduleFragment.kt +++ b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/ScheduleFragment.kt @@ -1,54 +1,55 @@ -package edu.rit.csh.bettervent +package edu.rit.csh.bettervent.view.kiosk import android.os.Bundle -import android.support.v4.app.Fragment +import android.util.Log +import androidx.fragment.app.Fragment import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.ViewModelProviders import com.alamkanak.weekview.DateTimeInterpreter import com.alamkanak.weekview.MonthLoader import com.alamkanak.weekview.WeekView import com.alamkanak.weekview.WeekViewDisplayable import com.alamkanak.weekview.WeekViewEvent -import com.google.api.services.calendar.model.Event +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.Event +import edu.rit.csh.bettervent.viewmodel.EventActivityViewModel -import java.io.Serializable import java.text.SimpleDateFormat import java.util.ArrayList import java.util.Calendar import java.util.Locale -class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { +class ScheduleFragment : Fragment(){ lateinit var weekView: WeekView - private var events: List? = null + private lateinit var viewModel: EventActivityViewModel + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + viewModel = ViewModelProviders.of(requireActivity()).get(EventActivityViewModel::class.java) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { infoPrint("Loaded Schedule Fragment.") val view = inflater.inflate(R.layout.fragment_schedule, container, false) - val args = arguments - if (args != null) { - infoPrint("Found events data") - events = args.getSerializable("events") as List - if (events != null && events!!.isNotEmpty()) - infoPrint("First event title: " + events!![0].summary) - } else { - infoPrint("ERROR! NO DATA FOUND!") - } - MainActivity.centralClock.setTextColor(-0x1000000) weekView = view.findViewById(R.id.week_view) - weekView.setMonthChangeListener(this as MonthLoader.MonthChangeListener) + weekView.setMonthChangeListener(MonthChangeListener() as MonthLoader.MonthChangeListener) weekView.numberOfVisibleDays = 7 // Lets change some dimensions to best fit the view. weekView.columnGap = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, resources.displayMetrics).toInt() weekView.setTimeColumnTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10f, resources.displayMetrics).toInt()) weekView.eventTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10f, resources.displayMetrics).toInt() + + setupDateTimeInterpreter() + return view } @@ -67,7 +68,7 @@ class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { if (weekView.numberOfVisibleDays == 7) { weekday = weekday[0].toString() } - return weekday.toUpperCase() + format.format(date.time) + return weekday.toUpperCase(Locale.getDefault()) + format.format(date.time) } override fun interpretTime(hour: Int): String { @@ -76,6 +77,24 @@ class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { } } + private fun getEvents(cal: Calendar): ArrayList{ + val month = cal.get(Calendar.MONTH) + val year = cal.get(Calendar.YEAR) + val events = arrayListOf() + + for (event in viewModel.events){ + val eventCal = Calendar.getInstance() + eventCal.timeInMillis = event.start.time + val eventMonth = eventCal.get(Calendar.MONTH) + val eventYear = eventCal.get(Calendar.YEAR) + + if (month == eventMonth && year == eventYear){ + events.add(event) + } + } + return events + } + protected fun getEventTitle(time: Calendar): String { val hour = time.get(Calendar.HOUR_OF_DAY) val minute = time.get(Calendar.MINUTE) @@ -85,26 +104,30 @@ class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { } private fun infoPrint(info: String) { - println("SCHE_: $info") + Log.i("ScheduleFragment", info) } - override fun onMonthChange(startDate: Calendar, endDate: Calendar): List> { + inner class MonthChangeListener: MonthLoader.MonthChangeListener{ - val weekViewEvents = ArrayList>() + override fun onMonthChange(startDate: Calendar, endDate: Calendar): List> { - val color1 = resources.getColor(R.color.colorPrimaryDark) + val weekViewEvents = ArrayList>() - if (events != null) { - infoPrint("event size : " + events!!.size) - for (i in events!!.indices) { - val event = events!![i] + val color1 = resources.getColor(R.color.colorPrimaryDark) + infoPrint("event size : " + viewModel.events.size) + + val events = getEvents(startDate) + for (i in events.indices) { + val event = viewModel.events[i] val wve = WeekViewEvent() + infoPrint(event.toString()) + // Set ID (not the Google Calendar ID). - wve.setId(i.toLong()) + wve.id = i.toLong() // Set Title - wve.setTitle(event.summary) + wve.title = event.summary val newYear = startDate.get(Calendar.YEAR) val newMonth = startDate.get(Calendar.MONTH) @@ -113,17 +136,18 @@ class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { try { // Start Time val startCal = Calendar.getInstance() - startCal.timeInMillis = event.start.dateTime.value + startCal.timeInMillis = event.start.time startCal.set(Calendar.MONTH, newMonth) startCal.set(Calendar.YEAR, newYear) - wve.setStartTime(startCal) + Log.i("ScheduleFragment", "Startcal: $startCal") + wve.startTime = startCal // End Time val endCal = Calendar.getInstance() - endCal.timeInMillis = event.end.dateTime.value + endCal.timeInMillis = event.end.time endCal.set(Calendar.MONTH, newMonth) endCal.set(Calendar.YEAR, newYear) - wve.setEndTime(endCal) + wve.endTime = endCal } catch (error: NullPointerException) { error.printStackTrace() wve.setIsAllDay(true) @@ -135,18 +159,7 @@ class ScheduleFragment : Fragment(), MonthLoader.MonthChangeListener { weekViewEvents.add(wve as WeekViewDisplayable) } - } - return weekViewEvents - } - - companion object { - - fun newInstance(events: List?): ScheduleFragment { - val f = ScheduleFragment() - val args = Bundle() - args.putSerializable("events", events as Serializable) - f.arguments = args - return f + return weekViewEvents } } } diff --git a/app/src/main/java/edu/rit/csh/bettervent/SettingsFragment.kt b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/SettingsFragment.kt similarity index 93% rename from app/src/main/java/edu/rit/csh/bettervent/SettingsFragment.kt rename to app/src/main/java/edu/rit/csh/bettervent/view/kiosk/SettingsFragment.kt index 1de7124..fc94819 100644 --- a/app/src/main/java/edu/rit/csh/bettervent/SettingsFragment.kt +++ b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/SettingsFragment.kt @@ -1,17 +1,14 @@ -package edu.rit.csh.bettervent +package edu.rit.csh.bettervent.view.kiosk import android.content.Context import android.content.SharedPreferences import android.os.Bundle -import android.support.v4.app.Fragment +import androidx.fragment.app.Fragment import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.EditText -import android.widget.RadioButton -import android.widget.RadioGroup -import kotlinx.android.synthetic.main.fragment_settings.* +import edu.rit.csh.bettervent.R import kotlinx.android.synthetic.main.fragment_settings.view.* class SettingsFragment : Fragment() { @@ -32,7 +29,7 @@ class SettingsFragment : Fragment() { infoPrint("Loaded Settings Fragment.") val view = inflater.inflate(R.layout.fragment_settings, container, false) - appSettings = context!!.getSharedPreferences( + appSettings = requireActivity().applicationContext!!.getSharedPreferences( getString(R.string.preference_file_key), Context.MODE_PRIVATE) Log.i("test", appSettings.getString(filterKeywordsString, "test")) @@ -45,8 +42,6 @@ class SettingsFragment : Fragment() { "Password: [REDACTED]" ) - MainActivity.centralClock.setTextColor(-0x1000000) - view.calendar_id_prompt.setText(appSettings.getString(calendarIDString, "")) view.max_results_prompt.setText(appSettings.getString(maxResultsString, "")) view.filtering_keywords_prompt.setText(appSettings.getString(filterKeywordsString, "")) diff --git a/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/StatusFragment.kt b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/StatusFragment.kt new file mode 100644 index 0000000..18e9e0f --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/view/kiosk/StatusFragment.kt @@ -0,0 +1,160 @@ +package edu.rit.csh.bettervent.view.kiosk + +import android.content.Context +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.ViewModelProviders +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.Event +import edu.rit.csh.bettervent.viewmodel.EventActivityViewModel + +import kotlinx.android.synthetic.main.fragment_status.* +import kotlinx.android.synthetic.main.password_alert.view.* +import org.jetbrains.anko.alert +import org.jetbrains.anko.noButton +import org.jetbrains.anko.yesButton + +import java.text.SimpleDateFormat +import java.util.* +import kotlin.system.exitProcess + +class StatusFragment : Fragment(){ + + private lateinit var appSettings: SharedPreferences // Settings object containing user preferences. + private lateinit var viewModel: EventActivityViewModel + private lateinit var listener: OpenSettingsListener + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + infoPrint("Loaded Status Fragment.") + + return inflater.inflate(R.layout.fragment_status, container, false) + } + + /** + * @param view + * @param savedInstanceState + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // Load up app settings to fetch passwords and background colors. + appSettings = requireActivity().applicationContext!!.getSharedPreferences( + getString(R.string.preference_file_key), Context.MODE_PRIVATE) + + viewModel = ViewModelProviders.of(requireActivity()).get(EventActivityViewModel::class.java) + + fun showAlertWithFunction(onSuccess: () -> Unit) { + context?.alert("Enter Password:") { + val v = layoutInflater.inflate(R.layout.password_alert, null) + customView = v + fun checkPassword(pw: String) { + if (pw == appSettings.getString("edu.rit.csh.bettervent.password", "")) onSuccess() + } + yesButton { checkPassword(v.password_et.text.toString()) } + noButton { dialog -> dialog.cancel() } + }?.show() + } + + leave_button.setOnClickListener { + showAlertWithFunction { exitProcess(0) } + } + + settings_button.setOnClickListener { + showAlertWithFunction { listener.openSettings() } + } + updateCurrentAndNextEventsInUI() + } + + override fun onAttach(context: Context) { + context.let { super.onAttach(it) + if (it is OpenSettingsListener) { + listener = it + } else { + throw ClassCastException("$it must implement OpenSettingsListener.") + } + } + } + + /** + * Looks at the APIOutList (the List of Events generated by the API), + * and based on how many there are and when they are, sets the string + * values for currentEventTitle, currentEventTime, nextEventTitle, and + * nextEventTime. + */ + + fun updateCurrentAndNextEventsInUI() { + + infoPrint("Updating UI") + + viewModel.events.also { + when { + it.isEmpty() -> { + setRoomAsEmpty(); setNoNextEvent() + } + it.size == 1 -> if (it[0].isHappeningNow) setCurrentEvent(it[0]) else setNextEvent(it[0]) + else -> { + if (it[0].isHappeningNow){ + setCurrentEvent(it[0]) + setNextEvent(it[1]) + } else { + setRoomAsEmpty() + setNextEvent(it[0]) + } + } + } + } + } + + private fun setRoomAsEmpty(){ + free_label.visibility = View.VISIBLE + reserved_label.visibility = View.INVISIBLE + event_title.text = "" + event_time.text = "" + status_layout.setBackgroundColor(resources.getColor(R.color.CSHGreen)) + } + + private fun setNoNextEvent(){ + next_label.visibility = View.INVISIBLE + next_event_time.text = "" + next_event_title.text = "There are no upcoming events." + } + + private fun setCurrentEvent(e: Event){ + free_label.visibility = View.INVISIBLE + reserved_label.visibility = View.VISIBLE + event_title.text = e.summary + event_time.text = "${formatDate(e.start)} - ${formatDate(e.end)}" + status_layout.setBackgroundColor(resources.getColor(R.color.CSHRed)) + } + + private fun setNextEvent(e: Event){ + next_label.visibility = View.VISIBLE + next_event_time.text = "${formatDate(e.start)} - ${formatDate(e.end)}" + next_event_title.text = e.summary + } + + /** + * Method to format DateTimes into human-readable strings + * + * @param dateTime: DateTime to make readable + * @return: HH:MM on YYYY/MM/DD + */ + private fun formatDate(inputDate: Date): String { + val simpleTimeFormat = SimpleDateFormat("HH:mm", Locale.getDefault()) + val simpleDateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()) + val time = simpleTimeFormat.format(inputDate) + val date = simpleDateFormat.format(inputDate) + return "$time on $date" + } + + private fun infoPrint(info: String) { + Log.i("StatusFragment", info) + } +} + +interface OpenSettingsListener{ + fun openSettings() +} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/viewmodel/CompanionActivityViewModel.kt b/app/src/main/java/edu/rit/csh/bettervent/viewmodel/CompanionActivityViewModel.kt new file mode 100644 index 0000000..3abf324 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/viewmodel/CompanionActivityViewModel.kt @@ -0,0 +1,152 @@ +package edu.rit.csh.bettervent.viewmodel + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import com.google.api.client.extensions.android.http.AndroidHttp +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException +import com.google.api.client.json.JsonFactory +import com.google.api.client.json.gson.GsonFactory +import com.google.api.client.util.DateTime +import com.google.api.client.util.ExponentialBackOff +import com.google.api.services.calendar.Calendar +import com.google.api.services.calendar.CalendarScopes +import com.google.api.services.calendar.model.CalendarList +import com.google.api.services.calendar.model.Events +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.CalendarInfo +import edu.rit.csh.bettervent.view.Event +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.util.* +import kotlin.collections.ArrayList + +class CompanionActivityViewModel(application: Application) : AndroidViewModel(application) { + + private val settings: SharedPreferences = + application.applicationContext.getSharedPreferences(application.getString(R.string.preference_file_key), Context.MODE_PRIVATE) + + private var mService: Calendar = getCalendarService() + + private val locationsString = settings.getString("locations", "")!! + + lateinit var calendarItems: List + val usedLocations = locationsString.split("|").filterNot { it.isBlank() }.toMutableSet() + val allLocations = mutableSetOf() + val eventsByLocation = mutableMapOf() + + init { + refreshCalendarOptions() + } + + fun refresh(onComplete: () -> Unit) { + updateEvents(onComplete) + } + + fun addUsedLocation(location: String) { + var locationsString = settings.getString("locations", "")!! + locationsString = "$locationsString|$location" + settings.edit().putString("locations", locationsString).apply() + usedLocations.add(location) + } + + fun removeUsedLocation(location: String) { + var locationsString = settings.getString("locations", "")!! + locationsString = locationsString.replace(location, "").replace("||", "") + settings.edit().putString("locations", locationsString).apply() + usedLocations.remove(location) + } + + private fun getCalendarService(): Calendar { + val transport = AndroidHttp.newCompatibleTransport() + val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance() + + val credential = GoogleAccountCredential.usingOAuth2( + getApplication(), listOf(*SCOPES)) + .setBackOff(ExponentialBackOff()) + .setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, "")) + + return Calendar.Builder( + transport, jsonFactory, credential) + .setApplicationName("Google Calendar API Android Quickstart") + .build() + } + + fun refreshCalendarOptions(){ + mService = getCalendarService() + doAsync { + val inItems = mService.calendarList().list().execute() + uiThread { + calendarItems = inItems.items.map { it.toCalendarInfo() } + } + } + } + + private fun updateEvents(f: () -> Unit) { + doAsync { + val events = getEventsFromServer(MAX_EVENTS) + uiThread { + handleEvents(parseEvents(events)) + f.invoke() + } + } + } + + private fun getEventsFromServer(maxEvents: Int): Events { + val calendarId = settings.getString("edu.rit.csh.bettervent.calendarid", "rti648k5hv7j3ae3a3rum8potk@group.calendar.google.com") + + val now = DateTime(System.currentTimeMillis()) + + return mService.events().list(calendarId) + .setMaxResults(maxEvents) + .setTimeMin(now) + .setOrderBy("startTime") + .setSingleEvents(true) + .execute() + } + + private fun parseEvents(calendarEvents: Events): MutableList { + val events = mutableListOf() + + events.addAll(calendarEvents.items.mapNotNull { calendarEvent -> + calendarEvent.location?.let { allLocations.add(it) } + calendarEvent.parseToEvent() + }) + Log.i("CompanionActivityViewModel", allLocations.toString()) + + return events + } + + private fun handleEvents(inEvents: Collection) { + eventsByLocation.clear() + eventsByLocation.putAll(usedLocations.map { location -> + location to inEvents.firstOrNull { event -> + event.location.trim().toLowerCase(Locale.getDefault()) == + location.trim().toLowerCase(Locale.getDefault()) + } + }) + } + + companion object { + private const val PREF_ACCOUNT_NAME = "accountName" + private const val MAX_EVENTS = 50 + private val SCOPES = arrayOf(CalendarScopes.CALENDAR_READONLY) + } + + private fun com.google.api.services.calendar.model.Event.parseToEvent(): Event? { + location?.also { + return Event(summary, + Date(start.dateTime.value), + Date(end.dateTime.value), + location) + } + return null + } + + private fun com.google.api.services.calendar.model.CalendarListEntry.toCalendarInfo(): CalendarInfo { + return CalendarInfo(summary, id) + } +} \ No newline at end of file diff --git a/app/src/main/java/edu/rit/csh/bettervent/viewmodel/EventActivityViewModel.kt b/app/src/main/java/edu/rit/csh/bettervent/viewmodel/EventActivityViewModel.kt new file mode 100644 index 0000000..87fb5b2 --- /dev/null +++ b/app/src/main/java/edu/rit/csh/bettervent/viewmodel/EventActivityViewModel.kt @@ -0,0 +1,128 @@ +package edu.rit.csh.bettervent.viewmodel + +import android.app.Application +import android.content.Context +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import com.google.api.client.extensions.android.http.AndroidHttp +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.json.JsonFactory +import com.google.api.client.json.gson.GsonFactory +import com.google.api.client.util.DateTime +import com.google.api.client.util.ExponentialBackOff +import com.google.api.services.calendar.Calendar +import com.google.api.services.calendar.CalendarScopes +import com.google.api.services.calendar.model.Events +import edu.rit.csh.bettervent.R +import edu.rit.csh.bettervent.view.Event +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread +import java.util.* +import kotlin.collections.ArrayList + +class EventActivityViewModel(application: Application) : AndroidViewModel(application) { + val events: ArrayList = arrayListOf() + + private val settings: SharedPreferences = + application.applicationContext.getSharedPreferences(application.getString(R.string.preference_file_key), Context.MODE_PRIVATE) + + private val mService = getCalendarService() + + fun refresh(onComplete: () -> Unit) { + updateEvents(onComplete) + } + + private fun getCalendarService(): Calendar { + Log.i("EventActivityViewModel", settings.getString("test", "test")!!) + val transport = AndroidHttp.newCompatibleTransport() + val jsonFactory: JsonFactory = GsonFactory.getDefaultInstance() + + val credential = GoogleAccountCredential.usingOAuth2( + getApplication(), listOf(*SCOPES)) + .setBackOff(ExponentialBackOff()) + .setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, "")) + + return Calendar.Builder( + transport, jsonFactory, credential) + .setApplicationName("Google Calendar API Android Quickstart") + .build() + } + + private fun updateEvents(f: () -> Unit){ + doAsync{ + val events = getEventsFromServer() + uiThread { + handleEvents(parseEvents(events)) + f.invoke() + } + } + } + + private fun getEventsFromServer(): Events { + val calendarId = settings.getString("edu.rit.csh.bettervent.calendarid", "rti648k5hv7j3ae3a3rum8potk@group.calendar.google.com") + + val maxResultsStr = settings.getString("edu.rit.csh.bettervent.maxresults", "100") + val maxResults = maxResultsStr?.let { Integer.parseInt(it) } + + val now = DateTime(System.currentTimeMillis()) + return mService.events().list(calendarId) + .setMaxResults(maxResults) + .setTimeMin(now) + .setOrderBy("startTime") + .setSingleEvents(true) + .execute() + } + + private fun parseEvents(calendarEvents: Events): ArrayList{ + val events = ArrayList() + for (calendarEvent in calendarEvents.items){ + val event = calendarEvent.parseToEvent() + event?.also{ + Log.i("MainActivity", "Event added: $event") + events.add(it) + } + } + return events + } + + private fun handleEvents(inEvents: ArrayList){ + events.removeAll(events) + + if (inEvents.isNotEmpty()){ + val eventKeyword = settings.getString("edu.rit.csh.bettervent.filterkeywords", "")!! + events.removeAll(events) + for (event in inEvents) { + val eventFieldToCheck = if (settings.getBoolean("edu.rit.csh.bettervent.filterbytitle", false)) { + event.summary + } else { + event.location + } + if (eventKeyword.isNotEmpty()) { + if (eventFieldToCheck.toLowerCase(Locale.getDefault()).contains(eventKeyword.toLowerCase(Locale.getDefault()))) { + events.add(event) + } + } else { + events.add(event) + } + } + } + } + + companion object { + private const val PREF_ACCOUNT_NAME = "accountName" + private val SCOPES = arrayOf(CalendarScopes.CALENDAR_READONLY) + } + + + + private fun com.google.api.services.calendar.model.Event.parseToEvent(): Event?{ + location?.also{ + return Event(summary, + Date(start.dateTime.value), + Date(end.dateTime.value), + location) + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/res/color/bottom_nav.xml b/app/src/main/res/color/bottom_nav.xml new file mode 100644 index 0000000..3d49bb6 --- /dev/null +++ b/app/src/main/res/color/bottom_nav.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..757f450 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..09503ae --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml deleted file mode 100644 index 70fb291..0000000 --- a/app/src/main/res/drawable/ic_home_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_more_vert.xml b/app/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 0000000..dcfdb14 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/rounded_rectangle.xml b/app/src/main/res/drawable/rounded_rectangle.xml new file mode 100644 index 0000000..2b64cf1 --- /dev/null +++ b/app/src/main/res/drawable/rounded_rectangle.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_companion.xml b/app/src/main/res/layout/activity_companion.xml new file mode 100644 index 0000000..48c6239 --- /dev/null +++ b/app/src/main/res/layout/activity_companion.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_event.xml b/app/src/main/res/layout/activity_event.xml new file mode 100644 index 0000000..19ed3e9 --- /dev/null +++ b/app/src/main/res/layout/activity_event.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5171c38..f78d8c1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,57 +1,95 @@ - + android:padding="20dp" + android:id="@+id/main_root" + android:visibility="invisible"> - + + + + + + +