diff --git a/.gitignore b/.gitignore index 67c2d736..d00e50bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +jniLibs +NDK # All below taken from: https://github.com/github/gitignore/blob/13c64104e96bbadc8c81b5e499386af443cca66b/Android.gitignore diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 34dc27cb..e96cda9b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,8 @@ + + @@ -28,6 +31,115 @@ + + + + diff --git a/.travis.yml b/.travis.yml index 832c4928..8cc1d8c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,5 +14,6 @@ before_install: script: - ./gradlew clean lint test + - ./gradlew connectedAndroidTest || true diff --git a/mobile/build.gradle b/mobile/build.gradle index c46808eb..52c1a890 100644 --- a/mobile/build.gradle +++ b/mobile/build.gradle @@ -5,13 +5,13 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { - compileSdkVersion 27 + compileSdkVersion 28 defaultConfig { applicationId "net.activitywatch.android" minSdkVersion 24 - targetSdkVersion 27 - versionCode 2 - versionName "0.1.0" + targetSdkVersion 28 + versionCode 5 + versionName "0.2.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { @@ -30,11 +30,15 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'com.android.support:appcompat-v7:27.1.1' - implementation 'com.android.support:support-v4:27.1.1' - implementation 'com.android.support:design:27.1.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:design:28.0.0' + implementation 'com.android.support:cardview-v7:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1' + implementation 'com.android.support:recyclerview-v7:28.0.0' + implementation 'android.arch.lifecycle:extensions:1.1.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt b/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt index 5e1a56c6..d9891b21 100644 --- a/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt +++ b/mobile/src/androidTest/java/net/activitywatch/android/ExampleInstrumentedTest.kt @@ -4,9 +4,12 @@ import android.support.test.InstrumentationRegistry import android.support.test.runner.AndroidJUnit4 import org.junit.Test +import org.junit.Before import org.junit.runner.RunWith import org.junit.Assert.* +import java.lang.Thread.sleep +import java.time.Instant /** * Instrumented test, which will execute on an Android device. @@ -19,6 +22,28 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getTargetContext() - assertEquals("net.activitywatch.aw_android", appContext.packageName) + assertEquals("net.activitywatch.android", appContext.packageName) + } + + @Test + fun getBuckets() { + // TODO: Clear test buckets before test + val appContext = InstrumentationRegistry.getTargetContext() + val ri = RustInterface(appContext) + val bucketId = "test-${Math.random()}" + val oldLen = ri.getBucketsJSON().length() + ri.createBucket("""{"id": "$bucketId", "type": "test", "hostname": "test", "client": "test"}""") + assertEquals(ri.getBucketsJSON().length(), oldLen + 1) + } + + @Test + fun createHeartbeat() { + val appContext = InstrumentationRegistry.getTargetContext() + val ri = RustInterface(appContext) + val bucketId = "test-${Math.random()}" + ri.createBucket("""{"id": "$bucketId", "type": "test", "hostname": "test", "client": "test"}""") + ri.heartbeat(bucketId, """{"timestamp": "${Instant.now()}", "duration": 0, "data": {"key": "value"}}""") + sleep(10) + assertEquals(1, ri.getEventsJSON(bucketId).length()) } } diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 23ae700f..10d59308 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -1,9 +1,11 @@ - - + ? = null // Handle navigation view item clicks here. when (item.itemId) { R.id.nav_dashboard -> { - Snackbar.make(coordinator_layout, "The dashboard button was clicked, but it's not yet implemented!", Snackbar.LENGTH_LONG) - .setAction("Action", null).show() + fragmentClass = TestFragment::class.java + } + R.id.nav_buckets -> { + fragmentClass = BucketListFragment::class.java } R.id.nav_settings -> { Snackbar.make(coordinator_layout, "The settings button was clicked, but it's not yet implemented!", Snackbar.LENGTH_LONG) @@ -141,6 +106,19 @@ class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelecte } } + val fragment: Fragment? = try { + fragmentClass?.newInstance() + } catch (e: Exception) { + e.printStackTrace() + null + } + + if(fragment != null) { + // Insert the fragment by replacing any existing fragment + val fragmentManager = supportFragmentManager + fragmentManager.beginTransaction().replace(R.id.fragment_container, fragment).commit() + } + drawer_layout.closeDrawer(GravityCompat.START) return true } diff --git a/mobile/src/main/java/net/activitywatch/android/RustInterface.kt b/mobile/src/main/java/net/activitywatch/android/RustInterface.kt new file mode 100644 index 00000000..f1917a62 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/RustInterface.kt @@ -0,0 +1,81 @@ +package net.activitywatch.android + +import android.content.Context +import android.util.Log +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.threeten.bp.Instant +import java.lang.Exception + +class RustInterface constructor(context: Context? = null) { + private val TAG = "RustGreetings" + + init { + System.loadLibrary("aw_server") + + if(context != null) { + setAndroidDataDir(context.filesDir.absolutePath) + } + } + + external fun greeting(pattern: String): String + external fun setAndroidDataDir(path: String) + external fun getBuckets(): String + external fun createBucket(bucket: String): String + external fun getEvents(bucket_id: String): String + external fun heartbeat(bucket_id: String, event: String): String + + fun sayHello(to: String): String { + return greeting(to) + } + + fun createBucketHelper(bucket_id: String, type: String, hostname: String = "unknown", client: String = "aw-android") { + if(bucket_id in getBucketsJSON().keys().asSequence()) { + Log.i(TAG, "Bucket with ID '$bucket_id', already existed. Not creating.") + } else { + val msg = createBucket("""{"id": "$bucket_id", "type": "$type", "hostname": "$hostname", "client": "$client"}"""); + Log.w(TAG, msg) + } + } + + // TODO: Implement handling of pulsetime + fun heartbeatHelper(bucket_id: String, timestamp: Instant, duration: Double, data: JSONObject, pulsetime: Double = 60.0) { + val event = """{"timestamp": "$timestamp", "duration": $duration, "data": $data}""" + val msg = heartbeat(bucket_id, event) + Log.w(TAG, msg) + } + + fun getBucketsJSON(): JSONObject { + // TODO: Handle errors + val json = JSONObject(getBuckets()) + if(json.length() <= 0) { + Log.w(TAG, "Length: ${json.length()}") + } + return json + } + + fun getEventsJSON(bucket_id: String, limit: Int = 0): JSONArray { + // TODO: Handle errors + // TODO: Use limit + return try { + JSONArray(getEvents(bucket_id)) + } catch(e: JSONException) { + Log.e(TAG, "Error when trying to fetch events from bucket, are you sure it exists?") + JSONArray() + } + } + + fun test() { + // TODO: Move to instrumented test + Log.w(TAG, sayHello("Android")) + createBucketHelper("test", "test") + Log.w(TAG, getBucketsJSON().toString(2)) + + val event = """{"timestamp": "${Instant.now()}", "duration": 0, "data": {"key": "value"}}""" + Log.w(TAG, event) + Log.w(TAG, heartbeat("test", event)) + Log.w(TAG, getBucketsJSON().toString(2)) + Log.w(TAG, getEventsJSON("test").toString(2)) + } +} \ No newline at end of file diff --git a/mobile/src/main/java/net/activitywatch/android/UsageStatsWatcher.kt b/mobile/src/main/java/net/activitywatch/android/UsageStatsWatcher.kt new file mode 100644 index 00000000..c80c9f36 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/UsageStatsWatcher.kt @@ -0,0 +1,158 @@ +package net.activitywatch.android + +import android.app.AppOpsManager +import android.app.usage.UsageEvents +import android.app.usage.UsageStatsManager +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.provider.Settings +import android.util.Log +import org.json.JSONObject +import java.text.SimpleDateFormat +import org.threeten.bp.DateTimeUtils +import org.threeten.bp.Instant +import java.sql.Timestamp +import java.text.ParseException + + +class UsageStatsWatcher constructor(val context: Context) { + private val TAG = "UsageStatsWatcher" + private val bucket_id = "aw-watcher-android-test" + + private val ri = RustInterface(context) + private val isoFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + + private fun isUsageAllowed(): Boolean { + // https://stackoverflow.com/questions/27215013/check-if-my-application-has-usage-access-enabled + val applicationInfo: ApplicationInfo = try { + context.packageManager.getApplicationInfo(context.packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(TAG, e.toString()) + return false + } + + val appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager + val mode = appOpsManager.checkOpNoThrow( + AppOpsManager.OPSTR_GET_USAGE_STATS, + applicationInfo.uid, + applicationInfo.packageName + ) + return mode == AppOpsManager.MODE_ALLOWED + } + + private fun getUSM(): UsageStatsManager? { + val usageIsAllowed = isUsageAllowed() + + return if (usageIsAllowed) { + // Get UsageStatsManager stuff + val usm: UsageStatsManager = context.getSystemService(Context.USAGE_STATS_SERVICE) as UsageStatsManager + usm + } else { + Log.w(TAG, "Was not allowed access to UsageStats, enable in settings.") + context.startActivity(Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS)) + null + } + } + + fun queryUsage() { + val usm = getUSM() + + if(usm != null) { + // Print per application + val usageStats = usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, 0, Long.MAX_VALUE) + Log.i(TAG, "usageStats.size=${usageStats.size}") + for(e in usageStats) { + Log.i(TAG, "${e.packageName}: ${e.totalTimeInForeground/1000}") + } + } + } + + // TODO: Move to seperate file in ./models/ + data class Event(val timestamp: Instant, val duration: Double = 0.0, val data: JSONObject) { + override fun toString(): String { + return """{"timestamp": "$timestamp", "duration": $duration, "data": $data}""" + } + } + + private fun createEventFromUsageEvent(uevent: UsageEvents.Event): Event { + val timestamp = DateTimeUtils.toInstant(java.util.Date(uevent.timeStamp)) + val pm = context.packageManager + val appName = try { + pm.getApplicationLabel(pm.getApplicationInfo(uevent.packageName, PackageManager.GET_META_DATA)) + } catch(e: PackageManager.NameNotFoundException) { + "Unknown" + } + return Event(timestamp = timestamp, duration = 0.0, data = JSONObject("""{"app": "$appName", "package": "${uevent.packageName}", "classname": "${uevent.className}"}""")) + } + + private fun getLastEvent(): JSONObject? { + val events = ri.getEventsJSON(bucket_id, limit=1) + return if (events.length() > 0) { + events[0] as JSONObject + } else { + null + } + } + + private fun getNewEvents(): List { + val usm = getUSM() + + // TODO: Get end time of last heartbeat + val lastEvent = getLastEvent() + Log.w(TAG, "Last event: $lastEvent") + + val since: Long = if(lastEvent != null) { + val timestampString = lastEvent.getString("timestamp") + // Instant.parse("2014-10-23T00:35:14.800Z").toEpochMilli() + try { + val timeCreatedDate = isoFormatter.parse(timestampString) + DateTimeUtils.toInstant(timeCreatedDate).toEpochMilli() + } catch (e: ParseException) { + Log.e(TAG, "Unable to parse timestamp") + 0L + } + } else { + 0L + } + + if(since == 0L) { + Log.e(TAG, "Since was 0, this should only happen on a fresh install") + } + + Log.w(TAG, "Since: $since") + + val newUsageEvents = mutableListOf() + if (usm != null) { + val usageEvents = usm.queryEvents(since, Long.MAX_VALUE) + while(usageEvents.hasNextEvent()) { + val eventOut = UsageEvents.Event() + usageEvents.getNextEvent(eventOut) + newUsageEvents.add(eventOut) + } + } + return newUsageEvents + } + + /*** + * Returns the number of events sent + */ + fun sendHeartbeats(): Int { + // Ensure bucket exists + ri.createBucketHelper(bucket_id, "test") + + var eventsSent = 0 + for(e in getNewEvents()) { + val awEvent = createEventFromUsageEvent(e) + // TODO: Use correct pulsetime, with long pulsetime if event was of some types (such as application close) + ri.heartbeatHelper(bucket_id, awEvent.timestamp, awEvent.duration, awEvent.data) + eventsSent++ + if(eventsSent >= 100) { + break + } + } + return eventsSent + } + +} \ No newline at end of file diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/BucketFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/BucketFragment.kt new file mode 100644 index 00000000..ce3e6b0c --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/fragments/BucketFragment.kt @@ -0,0 +1,34 @@ +package net.activitywatch.android.fragments + +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.support.v4.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup + +import net.activitywatch.android.R +import net.activitywatch.android.models.BucketViewModel + +class BucketFragment : Fragment() { + + companion object { + fun newInstance() = BucketFragment() + } + + private lateinit var viewModel: BucketViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.bucket_fragment, container, false) + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(this).get(BucketViewModel::class.java) + // TODO: Use the ViewModel + } + +} diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/BucketListFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/BucketListFragment.kt new file mode 100644 index 00000000..f77fea8a --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/fragments/BucketListFragment.kt @@ -0,0 +1,106 @@ +package net.activitywatch.android.fragments + +import android.content.Context +import android.os.Bundle +import android.support.v4.app.Fragment +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import net.activitywatch.android.R +import net.activitywatch.android.RustInterface +import net.activitywatch.android.models.BucketsContent +import org.json.JSONObject + + +/** + * A fragment representing a list of Items. + * Activities containing this fragment MUST implement the + * [BucketListFragment.OnListFragmentInteractionListener] interface. + */ +class BucketListFragment : Fragment() { + val TAG = "BucketListFragment" + + // TODO: Customize parameters + private var columnCount = 1 + + private var listener: OnListFragmentInteractionListener? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + arguments?.let { + columnCount = it.getInt(ARG_COLUMN_COUNT) + } + + BucketsContent.reload() + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_bucket_list, container, false) + + // Set the adapter + if (view is RecyclerView) { + with(view) { + layoutManager = when { + columnCount <= 1 -> LinearLayoutManager(context) + else -> GridLayoutManager(context, columnCount) + } + adapter = + MyBucketRecyclerViewAdapter(BucketsContent.ITEMS, listener) + } + } + return view + } + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnListFragmentInteractionListener) { + listener = context + } else { + throw RuntimeException(context.toString() + " must implement OnListFragmentInteractionListener") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + * + * + * See the Android Training lesson + * [Communicating with Other Fragments](http://developer.android.com/training/basics/fragments/communicating.html) + * for more information. + */ + interface OnListFragmentInteractionListener { + // TODO: Update argument type and name + fun onListFragmentInteraction(item: Bucket?) + } + + companion object { + + // TODO: Customize parameter argument names + const val ARG_COLUMN_COUNT = "column-count" + + // TODO: Customize parameter initialization + @JvmStatic + fun newInstance(columnCount: Int) = + BucketListFragment().apply { + arguments = Bundle().apply { + putInt(ARG_COLUMN_COUNT, columnCount) + } + } + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/MyBucketRecyclerViewAdapter.kt b/mobile/src/main/java/net/activitywatch/android/fragments/MyBucketRecyclerViewAdapter.kt new file mode 100644 index 00000000..31bee135 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/fragments/MyBucketRecyclerViewAdapter.kt @@ -0,0 +1,71 @@ +package net.activitywatch.android.fragments + +import android.support.v7.widget.RecyclerView +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView + + +import net.activitywatch.android.fragments.BucketListFragment.OnListFragmentInteractionListener + +import kotlinx.android.synthetic.main.fragment_bucket.view.* +import net.activitywatch.android.R +import org.json.JSONObject +import org.threeten.bp.DateTimeUtils +import org.threeten.bp.Instant +import org.threeten.bp.temporal.ChronoUnit +import java.text.SimpleDateFormat + +typealias Bucket = JSONObject + +/** + * [RecyclerView.Adapter] that can display a [DummyItem] and makes a call to the + * specified [OnListFragmentInteractionListener]. + * TODO: Replace the implementation with code for your data type. + */ +class MyBucketRecyclerViewAdapter( + private val mValues: List, + private val mListener: OnListFragmentInteractionListener? +) : RecyclerView.Adapter() { + + private val mOnClickListener: View.OnClickListener + + init { + mOnClickListener = View.OnClickListener { v -> + val item = v.tag as Bucket + // Notify the active callbacks interface (the activity, if the fragment is attached to + // one) that an item has been selected. + mListener?.onListFragmentInteraction(item) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.fragment_bucket, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val isoFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS'Z'") + val item = mValues[position] + holder.mIdView.text = item.getString("id") + holder.mContentView.text = DateTimeUtils.toInstant(isoFormatter.parse(item.getString("created"))).truncatedTo(ChronoUnit.DAYS).toString().subSequence(0, 10) + + with(holder.mView) { + tag = item + setOnClickListener(mOnClickListener) + } + } + + override fun getItemCount(): Int = mValues.size + + inner class ViewHolder(val mView: View) : RecyclerView.ViewHolder(mView) { + val mIdView: TextView = mView.item_number + val mContentView: TextView = mView.content + + override fun toString(): String { + return super.toString() + " '" + mContentView.text + "'" + } + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/fragments/TestFragment.kt b/mobile/src/main/java/net/activitywatch/android/fragments/TestFragment.kt new file mode 100644 index 00000000..fd392bc1 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/fragments/TestFragment.kt @@ -0,0 +1,58 @@ +package net.activitywatch.android.fragments + +import android.arch.lifecycle.ViewModelProviders +import android.os.Bundle +import android.support.design.widget.Snackbar +import android.support.v4.app.Fragment +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import kotlinx.android.synthetic.main.activity_main.* +import net.activitywatch.android.R +import net.activitywatch.android.RustInterface +import net.activitywatch.android.UsageStatsWatcher +import net.activitywatch.android.models.TestViewModel + + +class TestFragment : Fragment() { + + companion object { + fun newInstance() = TestFragment() + } + + private lateinit var viewModel: TestViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.test_fragment, container, false) + + val textView: TextView = view.findViewById(R.id.welcome_text) + textView.isClickable = true + textView.movementMethod = LinkMovementMethod.getInstance() + + return view + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + viewModel = ViewModelProviders.of(this).get(TestViewModel::class.java) + // TODO: Use the ViewModel + + val button = view?.findViewById(R.id.button) as Button + button.setOnClickListener { + val context = activity + if(context != null) { + val usw = UsageStatsWatcher(context) + val eventsSent = usw.sendHeartbeats() + Snackbar.make(context.findViewById(R.id.coordinator_layout), "Successfully saved $eventsSent new events to the database!${if (eventsSent >= 100) " (max 100 events saved at a time, spamming the button is not recommended)" else ""}", Snackbar.LENGTH_LONG) + .setAction("Action", null).show() + } + //testRust() + } + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/models/BucketViewModel.kt b/mobile/src/main/java/net/activitywatch/android/models/BucketViewModel.kt new file mode 100644 index 00000000..6a1c5ab9 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/models/BucketViewModel.kt @@ -0,0 +1,12 @@ +package net.activitywatch.android.models + +import android.arch.lifecycle.ViewModel; +import org.json.JSONObject + +class BucketViewModel(val json: JSONObject) : ViewModel() { + // TODO: Implement the ViewModel + init { + val id = json.getString("id") + //val eventCount = json.getInt("") + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/models/BucketsContent.kt b/mobile/src/main/java/net/activitywatch/android/models/BucketsContent.kt new file mode 100644 index 00000000..4d9cf72b --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/models/BucketsContent.kt @@ -0,0 +1,67 @@ +package net.activitywatch.android.models + +import android.util.Log +import net.activitywatch.android.RustInterface +import org.json.JSONObject +import java.util.ArrayList +import java.util.HashMap + +typealias Bucket = JSONObject + +/** + * Helper class for providing sample content for user interfaces created by + * Android template wizards. + * + * TODO: Replace all uses of this class before publishing your app. + */ +object BucketsContent { + + /** + * An array of sample (dummy) items. + */ + val ITEMS: MutableList = ArrayList() + + /** + * A map of sample (dummy) items, by ID. + */ + val ITEM_MAP: MutableMap = HashMap() + + val TAG = "BucketsContent" + + private val COUNT = 25 + + init { + reload() + } + + fun reload() { + Log.i(TAG, "Reloading buckets") + ITEMS.clear() + ITEM_MAP.clear() + val ri = RustInterface() + val buckets = ri.getBucketsJSON() + for(bid in buckets.keys()) { + addItem(buckets[bid] as JSONObject) + } + ITEMS.sortBy { it.getString("created") } + ITEMS.reverse() + } + + fun addItem(item: Bucket) { + ITEMS.add(item) + ITEM_MAP[item.getString("id")] = item + } + + private fun createDummyItem(position: Int): Bucket { + return Bucket("""{"id": "test $position", "created": "2019-01-01T16:20:00.1"}""") + } + + private fun makeDetails(position: Int): String { + val builder = StringBuilder() + builder.append("Details about Item: ").append(position) + for (i in 0 until position) { + builder.append("\nMore details information here.") + } + return builder.toString() + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/models/DummyContent.kt b/mobile/src/main/java/net/activitywatch/android/models/DummyContent.kt new file mode 100644 index 00000000..fd184b5b --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/models/DummyContent.kt @@ -0,0 +1,65 @@ +package net.activitywatch.android.models + +import java.util.ArrayList +import java.util.HashMap + +/** + * Helper class for providing sample content for user interfaces created by + * Android template wizards. + * + * TODO: Replace all uses of this class before publishing your app. + */ +object DummyContent { + + /** + * An array of sample (dummy) items. + */ + val ITEMS: MutableList = ArrayList() + + /** + * A map of sample (dummy) items, by ID. + */ + val ITEM_MAP: MutableMap = HashMap() + + private val COUNT = 25 + + init { + // Add some sample items. + for (i in 1..COUNT) { + addItem( + createDummyItem( + i + ) + ) + } + } + + private fun addItem(item: DummyItem) { + ITEMS.add(item) + ITEM_MAP.put(item.id, item) + } + + private fun createDummyItem(position: Int): DummyItem { + return DummyItem( + position.toString(), + "Item " + position, + makeDetails(position) + ) + } + + private fun makeDetails(position: Int): String { + val builder = StringBuilder() + builder.append("Details about Item: ").append(position) + for (i in 0..position - 1) { + builder.append("\nMore details information here.") + } + return builder.toString() + } + + /** + * A dummy item representing a piece of content. + */ + data class DummyItem(val id: String, val content: String, val details: String) { + override fun toString(): String = content + } +} diff --git a/mobile/src/main/java/net/activitywatch/android/models/TestViewModel.kt b/mobile/src/main/java/net/activitywatch/android/models/TestViewModel.kt new file mode 100644 index 00000000..2e75cdf3 --- /dev/null +++ b/mobile/src/main/java/net/activitywatch/android/models/TestViewModel.kt @@ -0,0 +1,7 @@ +package net.activitywatch.android.models + +import android.arch.lifecycle.ViewModel; + +class TestViewModel : ViewModel() { + // TODO: Implement the ViewModel +} diff --git a/mobile/src/main/res/layout/app_bar_main.xml b/mobile/src/main/res/layout/app_bar_main.xml index fbbe112a..978fa39e 100644 --- a/mobile/src/main/res/layout/app_bar_main.xml +++ b/mobile/src/main/res/layout/app_bar_main.xml @@ -13,11 +13,11 @@ android:theme="@style/AppTheme.AppBarOverlay"> + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="#FFFFFF" + app:popupTheme="@style/AppTheme.PopupOverlay" /> diff --git a/mobile/src/main/res/layout/bucket_fragment.xml b/mobile/src/main/res/layout/bucket_fragment.xml new file mode 100644 index 00000000..4e968e09 --- /dev/null +++ b/mobile/src/main/res/layout/bucket_fragment.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/mobile/src/main/res/layout/content_main.xml b/mobile/src/main/res/layout/content_main.xml index 58b5f011..31bfe263 100644 --- a/mobile/src/main/res/layout/content_main.xml +++ b/mobile/src/main/res/layout/content_main.xml @@ -1,5 +1,5 @@ - + tools:context=".MainActivity" android:id="@+id/fragment_container"> - - - - -