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

87
.gitignore vendored Normal file
View file

@ -0,0 +1,87 @@
# Built application files
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof

138
.idea/codeStyles/Project.xml generated Normal file
View file

@ -0,0 +1,138 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

25
.idea/jarRepositories.xml generated Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

9
.idea/misc.xml generated Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

12
.idea/runConfigurations.xml generated Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

View file

@ -1,2 +1,24 @@
# Revolut-Stock-List-Extract-Android # Stock-List-Extract-Android
Simple App to extract the list of ticker symbols from Revolut Simple App to extract the list of ticker symbols from R
## IF you need only the file: last updated at 2020.09.11
file: [stock_list_1599869417144.csv](https://github.com/fknives/Stock-List-Extract-Android/blob/dev/stock_list_1599869417144.csv)
## Usage:
- install the application on your phone
- Start the application
- Click on Start Service
- Enable Accessibility Service
- Click on Start Service again
- Notice a notification is shown
- Navigate to R All Stocks List
- Tap on the Notification
- Notice the app starts to load from the screen (the notification is updated)
- Wait until the notification disappears and the screen is scrolled until the end
- Go back to this app
- Notice it now shows all the tickers at the bottom and you can send the file to somewhere else
## Notes:
- the app does not have internet permission, so you don't have to worry that something is leaving your device that you do not want to
- after finishing the sync the AccessibilityService is disabled (at least above api 24, if you have a solution below please let me know)
- the file format is CSV meaning the tickers are separated by comma, easy to include into google sheets or other programs.

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" />

26
build.gradle Normal file
View file

@ -0,0 +1,26 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.0"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

21
gradle.properties Normal file
View file

@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Fri Sep 11 21:30:17 EEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

172
gradlew vendored Executable file
View file

@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

84
gradlew.bat vendored Normal file
View file

@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

2
settings.gradle Normal file
View file

@ -0,0 +1,2 @@
include ':app'
rootProject.name = "RStockList"

View file

@ -0,0 +1 @@
TSLA,AAPL,AAL,AMZN,MSFT,GOOGL,BA,DIS,DAL,KO,AMD,NFLX,BABA,FB,CCL,XOM,BYND,BRK.B,MA,T,SPCE,NCLH,V,GILD,ZM,F,UBER,RCL,MMM,NVDA,MCD,BKNG,GE,BAC,ATVI,MRO,NIO,PFE,GOOG,GRPN,AMRX,INTC,SBUX,ABEV,SHOP,GPRO,JPM,ADBE,WORK,CSCO,FOXA,NKE,AXP,SNE,APA,LUV,JNJ,OXY,ABBV,AMC,WFC,IVR,TWTR,MUR,BTG,AR,ANF,MFA,IBM,AIG,FIT,UAA,ABT,UAL,M,CVX,APPS,SNAP,QCOM,GM,HOME,PYPL,WMT,ZNGA,JBLU,NYMT,PINS,SM,AA,EB,ERJ,GOL,C,HLT,LXRX,PEP,SPOT,PLUG,PG,SQ,ARNC,TEVA,PBR,CVE,CRM,GT,TRIP,GOLD,MPC,MGM,FCAU,GES,BB,MAR,EA,EBAY,BIDU,O,AL,DOCU,ZNH,MGI,GPS,TWOU,MO,EXPE,GS,LPL,CHS,HAL,ET,SPGI,ROKU,UMC,KHC,TXMD,LMT,NET,AM,RACE,HPQ,LYFT,IMGN,ARI,TGI,JMIA,SPG,COTY,CLNY,EQH,ENLC,WM,NEE,AXL,FDX,VZ,DBX,MRK,MTDR,BSBR,SLB,AUY,MDRX,BEP,UPS,OPK,CAT,AGNC,SPWR,LEVI,XRX,DELL,HOG,CARS,TM,HMY,FSLR,TSM,JD,BBAR,AMAT,TTM,NRZ,CX,BX,FL,BLK,BMY,BBD,HPE,ALXN,IRBT,MDT,DDOG,CVS,AES,SLCA,PM,FVRR,BAM,PTON,DPZ,BSX,OKE,IVZ,MU,COP,EGO,ADSK,REGI,CLDR,REGN,AEO,GME,DXC,ARR,WBA,ALLY,VRTX,NBL,OVV,NOV,COST,BRFS,HIMX,CZR,ANGI,CYH,ORCL,LULU,CIM,DVN,CMCSA,WDC,TWO,CAJ,KSS,HAS,DHC,PBF,QD,MUX,TWLO,CL,BIIB,CNDT,STNE,PSX,EPD,TME,KGC,HON,H,WU,NEM,OKTA,NBEV,AU,AKAM,ETSY,NDAQ,BSMX,GFI,RL,AVGO,RY,PTEN,WPX,HMC,AMGN,DE,CHL,LLY,ILMN,SHAK,MELI,NVTA,HUBS,EL,BBY,ATUS,LTC,SID,NMR,WDAY,QSR,FOLD,CLF,BIP,HL,PANW,W,ITUB,UA,UNH,CTL,ALGN,CG,CRWD,WELL,VLO,UXIN,FWONK,DOW,AMT,SYY,FTNT,TRGP,K,TSN,TTWO,RUN,EMR,XEC,DUK,BEN,WRK,INFY,FTI,LVS,MCO,PAA,APD,FSK,ZEN,YPF,JKS,FLR,MSI,STX,MIK,STZ,A,MTCH,OSTK,NYT,USB,MCHP,WEN,PRU,EQIX,CBRE,NTNX,PAAS,MRVL,TD,LRCX,JWN,ADP,MET,SYF,MOMO,SFIX,MYL,KR,ECL,CAH,PS,PCG,MFG,BHC,WWE,LB,GGB,ANSS,WUBA,ADM,VALE,GEO,HLF,KMB,FVE,HDB,ON,INTU,LOW,MNST,CFG,XLNX,TJX,PE,FLEX,NOC,D,FCX,BF.B,DLR,PPL,SYK,MAT,EOG,AAP,SO,CHWY,BHP,SNPS,HUYA,DHR,BMA,VIPS,KMI,HCM,GLUU,VER,PBI,SKX,GIS,ZS,BOX,FEYE,EXEL,BG,KKR,ZTO,IBN,SUPV,VIV,EW,YUM,CC,WIT,SBSW,TIF,TCOM,MPLX,GRUB,NBIX,TIGR,IQ,TPR,DLTR,SCHW,LNG,ADI,PXD,CNX,ANET,BAX,RMD,VEEV,HRL,BJ,VGR,KAR,HSIC,QRTEA,DHI,ENIA,NKTR,PAM,CNC,NAVI,SHW,AOS,GDS,CGNX,IPG,BITA,ROK,HOLX,PHM,ATR,ESI,STAY,ITW,BAP,HTHT,TFX,MXIM,HBAN,COMM,MORN,HIG,CXO,JEF,ISBC,SSNC,MLCO,DRE,LKQ,TSU,FTV,TW,KDP,QLYS,LEN,NWSA,MMC,MAS,YUMC,KT,CSX,GO,OTIS,PWR,FITB,NTRS,VRSK,CBOE,SLM,INVH,FDS,CIEN,ORLY,GPK,WING,PGR,PFPT,ZION,PCAR,WAB,NSC,QTWO,PEG,NUVA,HUN,NYCB,JBHT,KNX,PBH,PRI,NOAH,SBS,BAH,FIS,UGP,DFS,LVGO,NKLA,NVAX,TDOC,AG,WMB,SEB,FAST,BDC,BK,FLT,ARCC,NVCR,BDX,X,SFM,Z,IBKR,CF,AVLR,CI,LYV,ATHM,LTHM,CY,HRB,SPLK,CTAS,DD,DG,MUFG,NTAP,ED,HST,ISRG,FE,UNM,NTCO,UNP,GD,HUM,BIO,GGAL,SKT,HD,AXTA,WTM,NTES,ODP,HWM,IP,BKR,IT,CABO,IDXX,ROST,BLL,MBT,UNIT,UCTT,CVNA,DRI,REAL,MDLZ,MDB,LX,ARMK,MS,SQM,SRE,ETFC,DVA,PD,EPAM,PH,CERN,BRX,FFIV,OLN,STT,ZBH,OMC,JNPR,IAG,VICI,RF,RH,TEAM,KEY,SE,SU,SWK,SWN,ICE,MKL,BVN,TECK,TV,CROX,BWA,CTSH,SWKS,KIM,VG,TMUS,CTVA,WB,WY,ALLT,MOS,ELAN,IGT,FSLY,XEL,CARR,MPW,CRSP,YY,IGMS,ULTA,CTXS,XGN,ZBRA,IIPR,ETRN,PLAN,ANTM,TAK,MTD,MTG,TAL,VFC,EDU,CAG,EFX,BMRN,NPTN,CCI,SMAR,RAD,NLOK,TER,CDE,CWEN,GLW,TFC,VIACA,CHGG,BILI,SOGO,EIX,INCY,TGT,IRM,GNL,LSCC,WYNN,GNW,ZTS,BIPC,SMFG,VMW,RES,CHKP,CIG,PLNT,PAYX,AEP,AFL,RLGY,INFN,PDD,SSSS,KTOS,TMO,CLVS,TROW,CLR,CLX,CMA,CME,CMG,EQR,EQT,PDCE,VST,LDOS,CNP,CSGP,ASML,VTR,COF,TPX,COG,BEPC,DISH,ALL,RNG,TRV,AME,AMH,TSG,CHTR,EVR,TTD,PLD,GNTX,EXC,MRNA,JKHY,APH,CDNS,VRSN,RRC,PNC,PBCT,KEYS,SEDG,LOMA,PPG,TXN,GDDY,RTX,NLY,DISCA,DISCK,FISV,HBI,FREQ,PSA,HCA,AVB,NOW,SCCO,HEI,SIRI,HES,COLD,NRG,HFC,GWPH,SGMO,LMND,AYX,AZO,HGV,NICE,WEX,NUE,DAN,STLD,AMTD,NVR,DNKN,EQNR,NWL,FHN,URBN,PSTG,BNTX,SBH
1 TSLA AAPL AAL AMZN MSFT GOOGL BA DIS DAL KO AMD NFLX BABA FB CCL XOM BYND BRK.B MA T SPCE NCLH V GILD ZM F UBER RCL MMM NVDA MCD BKNG GE BAC ATVI MRO NIO PFE GOOG GRPN AMRX INTC SBUX ABEV SHOP GPRO JPM ADBE WORK CSCO FOXA NKE AXP SNE APA LUV JNJ OXY ABBV AMC WFC IVR TWTR MUR BTG AR ANF MFA IBM AIG FIT UAA ABT UAL M CVX APPS SNAP QCOM GM HOME PYPL WMT ZNGA JBLU NYMT PINS SM AA EB ERJ GOL C HLT LXRX PEP SPOT PLUG PG SQ ARNC TEVA PBR CVE CRM GT TRIP GOLD MPC MGM FCAU GES BB MAR EA EBAY BIDU O AL DOCU ZNH MGI GPS TWOU MO EXPE GS LPL CHS HAL ET SPGI ROKU UMC KHC TXMD LMT NET AM RACE HPQ LYFT IMGN ARI TGI JMIA SPG COTY CLNY EQH ENLC WM NEE AXL FDX VZ DBX MRK MTDR BSBR SLB AUY MDRX BEP UPS OPK CAT AGNC SPWR LEVI XRX DELL HOG CARS TM HMY FSLR TSM JD BBAR AMAT TTM NRZ CX BX FL BLK BMY BBD HPE ALXN IRBT MDT DDOG CVS AES SLCA PM FVRR BAM PTON DPZ BSX OKE IVZ MU COP EGO ADSK REGI CLDR REGN AEO GME DXC ARR WBA ALLY VRTX NBL OVV NOV COST BRFS HIMX CZR ANGI CYH ORCL LULU CIM DVN CMCSA WDC TWO CAJ KSS HAS DHC PBF QD MUX TWLO CL BIIB CNDT STNE PSX EPD TME KGC HON H WU NEM OKTA NBEV AU AKAM ETSY NDAQ BSMX GFI RL AVGO RY PTEN WPX HMC AMGN DE CHL LLY ILMN SHAK MELI NVTA HUBS EL BBY ATUS LTC SID NMR WDAY QSR FOLD CLF BIP HL PANW W ITUB UA UNH CTL ALGN CG CRWD WELL VLO UXIN FWONK DOW AMT SYY FTNT TRGP K TSN TTWO RUN EMR XEC DUK BEN WRK INFY FTI LVS MCO PAA APD FSK ZEN YPF JKS FLR MSI STX MIK STZ A MTCH OSTK NYT USB MCHP WEN PRU EQIX CBRE NTNX PAAS MRVL TD LRCX JWN ADP MET SYF MOMO SFIX MYL KR ECL CAH PS PCG MFG BHC WWE LB GGB ANSS WUBA ADM VALE GEO HLF KMB FVE HDB ON INTU LOW MNST CFG XLNX TJX PE FLEX NOC D FCX BF.B DLR PPL SYK MAT EOG AAP SO CHWY BHP SNPS HUYA DHR BMA VIPS KMI HCM GLUU VER PBI SKX GIS ZS BOX FEYE EXEL BG KKR ZTO IBN SUPV VIV EW YUM CC WIT SBSW TIF TCOM MPLX GRUB NBIX TIGR IQ TPR DLTR SCHW LNG ADI PXD CNX ANET BAX RMD VEEV HRL BJ VGR KAR HSIC QRTEA DHI ENIA NKTR PAM CNC NAVI SHW AOS GDS CGNX IPG BITA ROK HOLX PHM ATR ESI STAY ITW BAP HTHT TFX MXIM HBAN COMM MORN HIG CXO JEF ISBC SSNC MLCO DRE LKQ TSU FTV TW KDP QLYS LEN NWSA MMC MAS YUMC KT CSX GO OTIS PWR FITB NTRS VRSK CBOE SLM INVH FDS CIEN ORLY GPK WING PGR PFPT ZION PCAR WAB NSC QTWO PEG NUVA HUN NYCB JBHT KNX PBH PRI NOAH SBS BAH FIS UGP DFS LVGO NKLA NVAX TDOC AG WMB SEB FAST BDC BK FLT ARCC NVCR BDX X SFM Z IBKR CF AVLR CI LYV ATHM LTHM CY HRB SPLK CTAS DD DG MUFG NTAP ED HST ISRG FE UNM NTCO UNP GD HUM BIO GGAL SKT HD AXTA WTM NTES ODP HWM IP BKR IT CABO IDXX ROST BLL MBT UNIT UCTT CVNA DRI REAL MDLZ MDB LX ARMK MS SQM SRE ETFC DVA PD EPAM PH CERN BRX FFIV OLN STT ZBH OMC JNPR IAG VICI RF RH TEAM KEY SE SU SWK SWN ICE MKL BVN TECK TV CROX BWA CTSH SWKS KIM VG TMUS CTVA WB WY ALLT MOS ELAN IGT FSLY XEL CARR MPW CRSP YY IGMS ULTA CTXS XGN ZBRA IIPR ETRN PLAN ANTM TAK MTD MTG TAL VFC EDU CAG EFX BMRN NPTN CCI SMAR RAD NLOK TER CDE CWEN GLW TFC VIACA CHGG BILI SOGO EIX INCY TGT IRM GNL LSCC WYNN GNW ZTS BIPC SMFG VMW RES CHKP CIG PLNT PAYX AEP AFL RLGY INFN PDD SSSS KTOS TMO CLVS TROW CLR CLX CMA CME CMG EQR EQT PDCE VST LDOS CNP CSGP ASML VTR COF TPX COG BEPC DISH ALL RNG TRV AME AMH TSG CHTR EVR TTD PLD GNTX EXC MRNA JKHY APH CDNS VRSN RRC PNC PBCT KEYS SEDG LOMA PPG TXN GDDY RTX NLY DISCA DISCK FISV HBI FREQ PSA HCA AVB NOW SCCO HEI SIRI HES COLD NRG HFC GWPH SGMO LMND AYX AZO HGV NICE WEX NUE DAN STLD AMTD NVR DNKN EQNR NWL FHN URBN PSTG BNTX SBH