This commit is contained in:
fknives 2020-09-12 03:22:09 +03:00 committed by Gergely Hegedus
parent 89cad899d9
commit 708b08f6eb
50 changed files with 1540 additions and 2 deletions

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

62
app/build.gradle Normal file
View file

@ -0,0 +1,62 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply from: "./config.gradle"
android {
compileSdkVersion 29
defaultConfig {
buildConfigField "String", "CONFIG_COMPANY_IMG_ID", "\"$COMPANY_IMG_ID\""
buildConfigField "String", "CONFIG_COMPANY_NAME_ID", "\"$COMPANY_NAME_ID\""
buildConfigField "String", "CONFIG_COMPANY_TICKER_ID", "\"$COMPANY_TICKER_ID\""
buildConfigField "String", "CONFIG_COMPANY_SHARE_PRICE_ID", "\"$COMPANY_SHARE_PRICE_ID\""
buildConfigField "String", "CONFIG_COMPANY_CHANGE_PERCENT_ID", "\"$COMPANY_CHANGE_PERCENT_ID\""
buildConfigField "String", "CONFIG_RECYCLER_ID1", "\"$RECYCLER_ID1\""
buildConfigField "String", "CONFIG_RECYCLER_ID2", "\"$RECYCLER_ID2\""
applicationId "org.fknives.rstocklist"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "androidx.recyclerview:recyclerview:1.1.0"
implementation "com.google.android.material:material:1.2.1"
testImplementation "junit:junit:4.13"
androidTestImplementation "androidx.test.ext:junit:1.1.2"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0"
}

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fknives.rstocklist">
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="org.fknives.rstocklist.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="org.fknives.rstocklist.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<service android:name="org.fknives.rstocklist.NotificationService" />
<service
android:name="org.fknives.rstocklist.appsync.SyncService"
android:label="Sync Rev Tickers"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/serviceconfig" />
</service>
</application>
</manifest>

View file

@ -0,0 +1,16 @@
package org.fknives.rstocklist
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class BindingViewHolder<Binding : ViewBinding>(
val binding: Binding
) : RecyclerView.ViewHolder(binding.root) {
constructor(
parent: ViewGroup,
howToBind: (LayoutInflater, ViewGroup, Boolean) -> Binding
) : this(howToBind(LayoutInflater.from(parent.context), parent, false))
}

View file

@ -0,0 +1,49 @@
package org.fknives.rstocklist
import android.content.Context
import androidx.annotation.MainThread
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import java.io.File
class FileManager private constructor(private val context: Context) {
private val fileDir get() = context.cacheDir
private var last = 0
private val stateFlow = MutableStateFlow(last)
val tickersWithLastLoadedAtFlow = stateFlow.map { loadTickersAndLastUpdatedAt() }
fun saveTickers(tickers: List<String>) {
context.cacheDir.listFiles()?.forEach { it.delete() }
val fileIntoSave = File(context.cacheDir, "$STATIC_PART_OF_FILENAME${System.currentTimeMillis()}$FILE_EXTENSION")
fileIntoSave.writeText(tickers.firstOrNull().orEmpty())
tickers.drop(1).forEach {
fileIntoSave.appendText(",$it")
}
last = (last + 1) % 2
stateFlow.value = last
}
private fun loadTickersAndLastUpdatedAt(): Pair<Long, List<String>>? =
lastFile()?.let {
it.getTimestampFromFile() to it.readText().split(",")
}
private fun File.getTimestampFromFile(): Long =
name.drop(STATIC_PART_OF_FILENAME.length).dropLast(FILE_EXTENSION.length).toLongOrNull() ?: 0L
fun lastFile(): File? =
fileDir.listFiles()
?.filter { it.name.contains(STATIC_PART_OF_FILENAME) }
?.maxByOrNull { it.getTimestampFromFile() }
companion object {
private const val STATIC_PART_OF_FILENAME = "stock_list_"
private const val FILE_EXTENSION = ".csv"
private var fileManager: FileManager? = null
@MainThread
operator fun invoke(context: Context): FileManager =
fileManager ?: FileManager(context.applicationContext).also { fileManager = it }
}
}

View file

@ -0,0 +1,69 @@
package org.fknives.rstocklist
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.collect
import org.fknives.rstocklist.appsync.SyncService
import org.fknives.rstocklist.databinding.ActivityMainBinding
import java.text.SimpleDateFormat
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val fileManager = FileManager(this)
binding.startServiceCta.setOnClickListener {
if (SyncService.canStart()) {
startService(NotificationService.getStartIntent(this))
} else {
startActivity(
Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
)
}
}
binding.shareFileCta.setOnClickListener {
val file = fileManager.lastFile() ?: return@setOnClickListener
val uri = FileProvider.getUriForFile(
this,
"org.fknives.rstocklist.fileprovider",
file
)
val sharingIntent = Intent(Intent.ACTION_SEND)
sharingIntent.type = "text/*"
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri)
startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.send_to)))
}
val adapter = TickerAdapter()
binding.recycler.adapter = adapter
lifecycleScope.launchWhenCreated {
fileManager.tickersWithLastLoadedAtFlow.collect {
binding.lastUpdatedAt.isVisible = it != null
binding.shareFileCta.isVisible = it != null
it?.first?.let(SimpleDateFormat("YYYY-MM-dd hh:mm")::format)
?.let { date -> getString(R.string.file_last_updated_at, date) }
?.let(binding.lastUpdatedAt::setText)
adapter.submitList(it?.second.orEmpty())
}
}
}
override fun onDestroy() {
super.onDestroy()
stopService(NotificationService.getStartIntent(this))
SyncService.stop()
}
}

View file

@ -0,0 +1,84 @@
package org.fknives.rstocklist
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import org.fknives.rstocklist.appsync.SyncService
class NotificationService : Service(), SyncService.EventListener {
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.start_service_channel_title)
val importance = NotificationManager.IMPORTANCE_HIGH
val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
mChannel.description = name
mChannel.enableLights(true)
val manager = (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
manager.createNotificationChannel(mChannel)
}
updateNotification("Click Me When on Ticker List Screen!")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getBooleanExtra(SYNC_STARTED, false)?.takeIf { it }?.let {
updateNotification("Syncing ...")
SyncService.listener = this
SyncService.start()
}
intent?.getIntExtra(PROGRESS, -1)?.takeIf { it >= 0 }?.let {
updateNotification("Processed: $it, syncing ...")
}
return START_NOT_STICKY
}
private fun updateNotification(text: String) {
val intent = Intent(this, NotificationService::class.java)
.putExtra(SYNC_STARTED, true)
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("Start syncing on Stock List")
.setContentText(text)
.setContentIntent(PendingIntent.getService(this, 0, intent, 0))
.build()
startForeground(1, notification)
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onItemProcessed(index: Int) {
updateNotification("Processed: ${index + 1}, syncing ...")
}
override fun onItemProcessingFinished(items: List<String>) {
FileManager(this).saveTickers(items)
updateNotification("Processed: ${items.size}.")
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
SyncService.stop()
}
companion object {
fun getStartIntent(context: Context): Intent =
Intent(context, NotificationService::class.java)
const val NOTIFICATION_CHANNEL_ID = "START_SERVICE_ID"
const val SYNC_STARTED = "SYNC_STARTED"
const val PROGRESS = "PROGRESS"
}
}

View file

@ -0,0 +1,32 @@
package org.fknives.rstocklist
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import org.fknives.rstocklist.databinding.ItemTickerBinding
class TickerAdapter :
ListAdapter<String, BindingViewHolder<ItemTickerBinding>>(StringDiffUtilItem()) {
class StringDiffUtilItem : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
true
override fun getChangePayload(oldItem: String, newItem: String): Any? =
this
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingViewHolder<ItemTickerBinding> =
BindingViewHolder(parent, ItemTickerBinding::inflate)
override fun onBindViewHolder(holder: BindingViewHolder<ItemTickerBinding>, position: Int) {
holder.binding.ticker.text = getItem(position)
}
}

View file

@ -0,0 +1,3 @@
package org.fknives.rstocklist
data class TickersWithLastLoadedTime(val tickers: List<String>, val lastLoadedAt: Long)

View file

@ -0,0 +1,34 @@
package org.fknives.rstocklist.appsync
import android.view.accessibility.AccessibilityNodeInfo
import org.fknives.rstocklist.BuildConfig
class ParseTicker {
operator fun invoke(accessibilityNodeInfo: AccessibilityNodeInfo): String? =
if (accessibilityNodeInfo.isStockViewGroup()) {
accessibilityNodeInfo.findAccessibilityNodeInfosByViewId(BuildConfig.CONFIG_COMPANY_TICKER_ID)
.firstOrNull()
?.text
?.toString()
} else {
null
}
private fun AccessibilityNodeInfo.isStockViewGroup(): Boolean =
companyIds.all { hasChildWithId(it) }
companion object {
private val companyIds = listOf(
BuildConfig.CONFIG_COMPANY_IMG_ID,
BuildConfig.CONFIG_COMPANY_NAME_ID,
BuildConfig.CONFIG_COMPANY_TICKER_ID,
BuildConfig.CONFIG_COMPANY_SHARE_PRICE_ID,
BuildConfig.CONFIG_COMPANY_CHANGE_PERCENT_ID
)
private fun AccessibilityNodeInfo.hasChildWithId(id: String): Boolean =
findAccessibilityNodeInfosByViewId(id)?.isNotEmpty() == true
}
}

View file

@ -0,0 +1,85 @@
package org.fknives.rstocklist.appsync
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import org.fknives.rstocklist.BuildConfig
class SyncService : AccessibilityService() {
private var traverseRecyclerView: TraverseRecyclerView? = null
private val parseTicker = ParseTicker()
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (syncState == SyncState.STOP){
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
disableSelf()
}
return
}
synchronized(this) {
isStarted = true
if (syncState == SyncState.NOT_STARTED) return
if (syncState == SyncState.RESET) {
syncState = SyncState.WORKING
traverseRecyclerView = object : TraverseRecyclerView(0) {
val tickers = mutableListOf<String>()
override fun found(accessibilityNodeInfo: AccessibilityNodeInfo) {
parseTicker(accessibilityNodeInfo)?.let(tickers::add)
listener?.onItemProcessed(tickers.size - 1)
}
override fun finished() {
syncState = SyncState.NOT_STARTED
listener?.onItemProcessingFinished(tickers)
}
}
}
val recycler = findRecycler() ?: return
traverseRecyclerView?.next(recycler)
}
}
override fun onInterrupt() {
isStarted = false
}
private fun findRecycler() =
RECYCLER_VIEW_IDS.asSequence().mapNotNull {
rootInActiveWindow.findAccessibilityNodeInfosByViewId(it)?.firstOrNull()
}.firstOrNull()
enum class SyncState {
RESET, WORKING, NOT_STARTED, STOP
}
interface EventListener {
fun onItemProcessed(index: Int)
fun onItemProcessingFinished(items: List<String>)
}
companion object {
private val RECYCLER_VIEW_IDS = listOf(
BuildConfig.CONFIG_RECYCLER_ID1,
BuildConfig.CONFIG_RECYCLER_ID2
)
private var isStarted: Boolean = false
private var syncState: SyncState = SyncState.NOT_STARTED
var listener : EventListener? = null
fun start() {
syncState = SyncState.RESET
}
fun stop() {
syncState = SyncState.STOP
}
fun canStart(): Boolean = isStarted
}
}

View file

@ -0,0 +1,98 @@
package org.fknives.rstocklist.appsync
import android.view.accessibility.AccessibilityNodeInfo
abstract class TraverseRecyclerView(private var index: Int) {
fun next(recyclerView: AccessibilityNodeInfo) {
val collectionRowCount = recyclerView.collectionInfo?.rowCount ?: return
if (collectionRowCount <= index) {
return finished()
}
val found = recyclerView.children
.firstOrNull { it.getRefreshedFlattenIndex() == index }
when {
found == null
&& recyclerView.getSmallestFlatIndexOfChildren(index) > index
&& recyclerView.canScrollBackward() -> {
recyclerView.scrollBackward()
}
found == null && recyclerView.canScrollForward() -> {
recyclerView.scrollForward()
}
found == null && index == collectionRowCount - 1 -> {
finished()
}
found == null -> {
if (recyclerView.canScrollBackward()) {
recyclerView.scrollBackward()
} else {
finished()
}
}
else -> {
index++
found(found)
}
}
}
protected abstract fun found(accessibilityNodeInfo: AccessibilityNodeInfo)
protected abstract fun finished()
companion object {
private fun AccessibilityNodeInfo.getRefreshedFlattenIndex(): Int? {
val maxColumnCount = parent.collectionInfo?.columnCount ?: return null
return apply { refresh() }
.collectionItemInfo
?.getFlattenIndex(maxColumnCount)
}
private fun AccessibilityNodeInfo.getSmallestFlatIndexOfChildren(default: Int): Int =
children.mapNotNull { it.getFlattenIndex() }
.minOrDefault(default)
private val AccessibilityNodeInfo.children
get() = (0 until childCount).mapNotNull(::getChild)
private fun AccessibilityNodeInfo.getFlattenIndex(): Int? {
val maxColumnCount = parent.collectionInfo?.columnCount ?: return null
return collectionItemInfo?.getFlattenIndex(maxColumnCount)
}
private fun AccessibilityNodeInfo.CollectionItemInfo.getFlattenIndex(maxColumnCount: Int) =
rowIndex
private fun <T : Comparable<T>> List<T>.minOrDefault(default: T) = minOrNull() ?: default
private fun AccessibilityNodeInfo.scrollBackward(delay: Long = 500): Boolean =
performActionThenDelayIfOk(delay) {
performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD.id)
}
private fun AccessibilityNodeInfo.scrollForward(delay: Long = 500): Boolean =
performActionThenDelayIfOk(delay) {
performAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.id)
}
private inline fun performActionThenDelayIfOk(delay: Long, action: () -> Boolean): Boolean {
val result = action()
if (result) {
Thread.sleep(delay)
}
return result
}
private fun AccessibilityNodeInfo.canScrollBackward(): Boolean =
canDo(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD.id)
private fun AccessibilityNodeInfo.canScrollForward(): Boolean =
canDo(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD.id)
private fun AccessibilityNodeInfo.canDo(id: Int): Boolean =
actionList?.any { it.id == id } == true
}
}

View file

@ -0,0 +1,31 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF"/>
</vector>

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/default_activity_padding"
android:paddingEnd="@dimen/default_activity_padding"
tools:context=".MainActivity">
<TextView
android:id="@+id/description"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_usage_description"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/start_service_cta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_padding"
android:text="@string/start_service_cta"
app:layout_constraintEnd_toStartOf="@id/share_file_cta"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/description" />
<Button
android:id="@+id/share_file_cta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_padding"
android:text="@string/share_file"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/start_service_cta"
app:layout_constraintTop_toBottomOf="@id/description" />
<TextView
android:id="@+id/last_updated_at"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/default_padding"
android:layout_marginBottom="@dimen/default_padding"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/start_service_cta" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/last_updated_at" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ticker"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#6200EE</color>
<color name="colorPrimaryDark">#3700B3</color>
<color name="colorAccent">#03DAC5</color>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="default_activity_padding">24dp</dimen>
<dimen name="default_padding">16dp</dimen>
</resources>

View file

@ -0,0 +1,18 @@
<resources>
<string name="app_name">RStockList</string>
<string name="app_usage_description">
This is a simple app to load all the stock tickers from Rev.\n
Clicking on the "Start Service" button will first navigate you to settings. You need to activate this app\'s AccessibilityService.
Next clicking the "Start Service" button will show a notification, then you need to navigate to Rev to the list of stocks.\n
At that point you should click Start Loading. Now the it will start to sync the data from Rev.\n
You should leave your phone for a few minutes.\n
When the sync finished the notification disappears and data is saved into a csv file.\n
You may use that file to import into Google Sheet or other platform where you want to verify and filter that data.\n
This file sharing is done from this screen, the tickers will be also shown\n
</string>
<string name="start_service_cta">Start Service</string>
<string name="file_last_updated_at">File last updated at: %s</string>
<string name="share_file">Send File</string>
<string name="start_service_channel_title">Start Service</string>
<string name="send_to">Send to</string>
</resources>

View file

@ -0,0 +1,10 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View file

@ -0,0 +1,3 @@
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="cache" path="."/>
</paths>

View file

@ -0,0 +1,7 @@
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowContentChanged|typeWindowStateChanged|typeViewFocused|typeViewSelected|typeViewScrolled"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagIncludeNotImportantViews|flagReportViewIds|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows"
android:canRetrieveWindowContent="true"
android:notificationTimeout="0"
android:settingsActivity="org.fknives.rstocklist.MainActivity" />