├── COPYING ├── README ├── app ├── build.gradle ├── libs │ └── poi-all.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── unwrappedapps │ │ └── android │ │ └── spreadsheet │ │ ├── ExampleInstrumentedTest.kt │ │ └── ScrollTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ │ └── com │ │ │ └── unwrappedapps │ │ │ └── android │ │ │ └── spreadsheet │ │ │ ├── PullState.kt │ │ │ ├── SearchTask.kt │ │ │ ├── SheetActivity.kt │ │ │ ├── spreadsheet │ │ │ ├── Cell.kt │ │ │ ├── Row.kt │ │ │ ├── Sheet.kt │ │ │ ├── Spreadsheet.kt │ │ │ ├── Workbook.kt │ │ │ ├── csv │ │ │ │ ├── CsvCell.kt │ │ │ │ ├── CsvRow.kt │ │ │ │ ├── CsvSheet.kt │ │ │ │ └── CsvWorkbook.kt │ │ │ ├── ods │ │ │ │ ├── OdsCell.kt │ │ │ │ ├── OdsRow.kt │ │ │ │ ├── OdsSheet.kt │ │ │ │ └── OdsWorkbook.kt │ │ │ └── poi │ │ │ │ ├── PoiCell.kt │ │ │ │ ├── PoiRow.kt │ │ │ │ ├── PoiSheet.kt │ │ │ │ └── PoiWorkbook.kt │ │ │ └── ui │ │ │ └── sheet │ │ │ ├── JumpToCellFragment.kt │ │ │ ├── SheetAdapter.kt │ │ │ ├── SheetFragment.kt │ │ │ ├── SheetLayoutManager.kt │ │ │ ├── SheetViewModel.kt │ │ │ └── SheetViewPager.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxxhdpi │ │ ├── ic_find_in_page_black_18dp.png │ │ └── ic_find_in_page_black_36dp.png │ │ ├── drawable │ │ ├── file16.png │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── graph_cell.xml │ │ ├── jump_dialog.xml │ │ ├── sheet_fragment.xml │ │ └── tab_activity.xml │ │ ├── menu │ │ └── menu_spreadsheet.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── searchable.xml │ └── test │ └── java │ └── com │ └── unwrappedapps │ └── android │ └── spreadsheet │ ├── ExampleUnitTest.kt │ └── PullTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Android Spreadsheet 2 | 3 | Copyright 2013-2019 Unwrapped Inc. 4 | 5 | Released under the GPLv2 License (see COPYING). 6 | 7 | This is a spreadsheet for Android. I began working on it in 2011, and 8 | have worked on it on and off since then. 9 | 10 | Currently it can load CSV, XLS, XLSX and ODS spreadsheets. 11 | 12 | I have functionality to save files, although I will work on that more before 13 | pushing it up. Other things are a priority before doing that right. 14 | 15 | There are a lot of little things that need to be done. I will work on them 16 | as I can, and accept pull requests and patches as well. 17 | 18 | If you send a pull request or patch, have the license for the patch be 19 | Apache License, Version 2.0. Include the license boilerplate as a 20 | comment at the beginning of the pull request. You can get that here 21 | https://www.apache.org/licenses/LICENSE-2.0#apply . Replace the 22 | brackets with the current year and copyright owner, which is yourself 23 | (or you can also assign copyright to Unwrapped Inc., although this is 24 | not required). 25 | 26 | (If you want, you can dual-license your patch as Apache v2 and GPLv2). 27 | 28 | The XLS/XLSX spreadsheet loading is done via the Apache POI library, 29 | which is under the Apache License, Version 2.0. 30 | 31 | If you wish to e-mail me, send mail to Dennis Sheil at 32 | sheet2019@unwrappedapps.com . 33 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | android { 8 | compileSdkVersion 28 9 | defaultConfig { 10 | applicationId "com.unwrappedapps.android.spreadsheet" 11 | // for POI 4.0.1 12 | minSdkVersion 26 13 | targetSdkVersion 28 14 | versionCode 1 15 | versionName "1.0" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | // for POI 4.0.1 25 | //compileOptions { 26 | // sourceCompatibility 1.8 27 | // targetCompatibility 1.8 28 | //} 29 | } 30 | 31 | dependencies { 32 | implementation 'com.google.android.material:material:1.0.0' 33 | implementation fileTree(include: ['*.jar'], dir: 'libs') 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 35 | implementation 'androidx.appcompat:appcompat:1.0.2' 36 | implementation 'androidx.recyclerview:recyclerview:1.0.0' 37 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 38 | implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' 39 | testImplementation 'junit:junit:4.12' 40 | androidTestImplementation 'androidx.test:runner:1.1.1' 41 | androidTestImplementation 'androidx.test:rules:1.1.1' 42 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 43 | implementation files('libs/poi-all.jar') 44 | } 45 | -------------------------------------------------------------------------------- /app/libs/poi-all.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/libs/poi-all.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/unwrappedapps/android/spreadsheet/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.unwrappedapps.android.spreadsheet", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/unwrappedapps/android/spreadsheet/ScrollTest.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import androidx.test.espresso.Espresso.onView 4 | import androidx.test.espresso.action.ViewActions.* 5 | import androidx.test.espresso.matcher.ViewMatchers.withId 6 | import androidx.test.rule.ActivityTestRule 7 | import androidx.test.runner.AndroidJUnit4 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.runner.RunWith 11 | 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | class ScrollTest { 15 | 16 | // added JvmField annotation because Kotlin seems to want it... 17 | @Rule 18 | @JvmField 19 | 20 | var mActivityRule : ActivityTestRule = ActivityTestRule(SheetActivity::class.java) 21 | 22 | @Test 23 | fun swipeAgain() { 24 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 25 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 26 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 27 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 28 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 29 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 30 | onView(withId(R.id.recyclerview)).perform(swipeLeft()) 31 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 32 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 33 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 34 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 35 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 36 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 37 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 38 | onView(withId(R.id.recyclerview)).perform(swipeUp()) 39 | onView(withId(R.id.recyclerview)).perform(swipeRight()) 40 | onView(withId(R.id.recyclerview)).perform(swipeRight()) 41 | onView(withId(R.id.recyclerview)).perform(swipeDown()) 42 | onView(withId(R.id.recyclerview)).perform(swipeDown()) 43 | // onView(withId(R.id.recyclerview)).perform(swipeLeft()) 44 | onView(withId(R.id.recyclerview)).perform(longClick()) 45 | onView(withId(R.id.recyclerview)).perform(longClick()) 46 | onView(withId(R.id.recyclerview)).perform(longClick()) 47 | onView(withId(R.id.recyclerview)).perform(longClick()) 48 | onView(withId(R.id.recyclerview)).perform(longClick()) 49 | onView(withId(R.id.recyclerview)).perform(longClick()) 50 | onView(withId(R.id.recyclerview)).perform(longClick()) 51 | onView(withId(R.id.recyclerview)).perform(longClick()) 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/PullState.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | enum class Pull { 3 | RUNNING, 4 | SUCCESSACK, 5 | SUCCESSNAK, 6 | FAILED 7 | } 8 | 9 | @Suppress("DataClassPrivateConstructor") 10 | data class PullState private constructor( 11 | val pull: Pull, 12 | val msg: String? = null) { 13 | companion object { 14 | val LOADED_ACK = PullState(Pull.SUCCESSACK) 15 | val LOADED_NAK = PullState(Pull.SUCCESSNAK) 16 | val LOADING = PullState(Pull.RUNNING) 17 | fun error(msg: String?) = PullState(Pull.FAILED, msg) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/SearchTask.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import android.os.AsyncTask 4 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Spreadsheet 5 | import java.lang.ref.WeakReference 6 | 7 | 8 | class SearchTask 9 | internal constructor(context: SheetActivity) : AsyncTask() { 10 | 11 | private val weakContext : WeakReference = WeakReference(context) 12 | 13 | var searchRow = 0 14 | var searchColumn = 0 15 | var searchLowerLast : String = "" 16 | 17 | override fun doInBackground(vararg params: SheetActivity.SearchData?) { 18 | 19 | val searchData = params[0] 20 | 21 | if (searchData != null) { 22 | doSearch(searchData.string, searchData.sheet, searchData.lastSearch) 23 | } 24 | } 25 | 26 | fun doSearch(string: String, spreadsheet: Spreadsheet, lastSearch : SheetActivity.LastSearch) { 27 | // case sensitive/insensitive 28 | // whole/partial word 29 | val wholeWord = false 30 | val caseSensitive = false 31 | 32 | val find = string.toLowerCase() 33 | 34 | val workbook = spreadsheet.workbook 35 | val currentSheet = workbook.currentSheet 36 | val sheet = workbook.sheetList[currentSheet] 37 | 38 | val startRow : Int 39 | var startColumn : Int 40 | 41 | var cellValue : String 42 | var first = true 43 | 44 | 45 | if (lastSearch.word.contains(find)) { 46 | startRow = lastSearch.row 47 | startColumn = lastSearch.column + 1 48 | } else { 49 | startRow = 0 50 | startColumn = 0 51 | } 52 | 53 | for (ri in startRow until sheet.rowList.size) { 54 | for (ci in startColumn until sheet.rowList[ri].cellList.size){ 55 | cellValue = sheet.rowList[ri].cellList[ci].cellValue.toLowerCase() 56 | if (first && !wholeWord && !caseSensitive && cellValue.contains(find)) { 57 | first = false 58 | searchRow = ri 59 | searchColumn = ci 60 | searchLowerLast = cellValue 61 | } 62 | } 63 | startColumn=0 64 | } 65 | } 66 | 67 | override fun onPostExecute(result: Unit?) { 68 | super.onPostExecute(result) 69 | weakContext.get()?.lastSearch = SheetActivity.LastSearch(searchRow, searchColumn, searchLowerLast) 70 | weakContext.get()?.doSearchJump() 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/SheetActivity.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import android.os.Bundle 7 | import android.view.Menu 8 | import android.view.MenuItem 9 | import android.app.SearchManager 10 | import android.content.Context 11 | import android.net.Uri 12 | import android.view.View 13 | import androidx.appcompat.widget.SearchView 14 | import androidx.fragment.app.Fragment 15 | import androidx.fragment.app.FragmentManager 16 | import androidx.fragment.app.FragmentStatePagerAdapter 17 | import androidx.lifecycle.Observer 18 | import androidx.lifecycle.ViewModelProviders 19 | import androidx.viewpager.widget.ViewPager 20 | import com.google.android.material.tabs.TabLayout 21 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Spreadsheet 22 | import com.unwrappedapps.android.spreadsheet.ui.sheet.* 23 | import kotlinx.android.synthetic.main.tab_activity.* 24 | 25 | 26 | class SheetActivity : AppCompatActivity() { 27 | 28 | lateinit var mPager: SheetViewPager 29 | var mSpreadsheetStatePagerAdapter: SpreadsheetStatePagerAdapter? = null 30 | 31 | var tabCount = 1 32 | 33 | var selectedTab = 0 34 | 35 | val MY_REQ_READ_EXTERNAL_STORAGE : Int = 93 // random magic number 36 | 37 | var lastSearch : LastSearch = resetLastSearch() 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | 42 | setContentView(R.layout.tab_activity) 43 | 44 | tabSetup() 45 | 46 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 47 | 48 | viewModel.sheetLoadState.observe(this , Observer { 49 | postSpreadsheetLoad() 50 | }) 51 | 52 | checkViewIntents(intent) 53 | } 54 | 55 | fun checkViewIntents(intent: Intent?) { 56 | if (intent == null) return 57 | val action = intent.action 58 | if (Intent.ACTION_VIEW.equals(action)) { 59 | val type = intent.type 60 | if (type != null) { 61 | val uri = intent.data 62 | if (uri != null) { 63 | val csvMime = "text/comma-separated-values" 64 | val odsMime = "application/vnd.oasis.opendocument.spreadsheet" 65 | val xlsMime = "application/vnd.ms-excel" 66 | val xlsxMime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" 67 | 68 | if (type.equals(csvMime)) { 69 | handleViewIntent(uri) 70 | } else if (type.equals(odsMime)) { 71 | handleViewIntent(uri) 72 | } else if (type.equals(xlsMime) || type.equals(xlsxMime)) { 73 | handleViewIntent(uri) 74 | } 75 | } 76 | } 77 | } 78 | // else if (Intent.ACTION_SEARCH.equals(action)) 79 | } 80 | 81 | fun handleViewIntent(uri : Uri) { 82 | 83 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 84 | viewModel.processUri(uri, contentResolver) 85 | postSpreadsheetLoad() 86 | } 87 | 88 | private fun tabSetup() { 89 | 90 | val numberOfTabs = tabCount 91 | 92 | val tabLayout = findViewById(R.id.tab_layout) as TabLayout 93 | val tab = arrayOfNulls(numberOfTabs) 94 | 95 | for (i in 0 until numberOfTabs) { 96 | tab[i] = tabLayout.newTab() 97 | } 98 | 99 | for (i in 0 until numberOfTabs) { 100 | tabLayout.addTab(tab[i]!!) 101 | } 102 | 103 | // create Fragment Pager Adapter 104 | //FragmentManager.enableDebugLogging(true); 105 | 106 | mSpreadsheetStatePagerAdapter = SpreadsheetStatePagerAdapter( 107 | supportFragmentManager) 108 | 109 | mPager = view_pager 110 | 111 | // attach adapter to viewpager 112 | mPager.setAdapter(mSpreadsheetStatePagerAdapter) 113 | 114 | // attach viewpager to tab layout 115 | tabLayout.setupWithViewPager(mPager) 116 | 117 | // action to do when page changed 118 | setViewPagerListener(mPager) 119 | } 120 | 121 | fun postSpreadsheetLoad() { 122 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 123 | 124 | val spreadsheet = viewModel.spreadsheet.value 125 | 126 | if (spreadsheet != null) { 127 | 128 | val tableCount = spreadsheet.workbook.sheetList.size 129 | 130 | val list = mutableListOf() 131 | 132 | for (i in 0 until tableCount) { 133 | val name = spreadsheet.workbook.sheetList[i].name 134 | list.add(name) 135 | } 136 | 137 | val numberOfTabs = tableCount 138 | 139 | val tabLayout = findViewById(R.id.tab_layout) as TabLayout 140 | val tab = arrayOfNulls(numberOfTabs) 141 | 142 | mSpreadsheetStatePagerAdapter?.setNames(list) 143 | 144 | mSpreadsheetStatePagerAdapter?.notifyDataSetChanged() 145 | 146 | tabLayout.removeAllTabs() 147 | 148 | for (i in 0 until numberOfTabs) { 149 | val name = spreadsheet.workbook.sheetList[i].name 150 | val newTab = tabLayout.newTab() 151 | newTab.setText(name) 152 | tab[i] = newTab 153 | } 154 | 155 | for (i in 0 until numberOfTabs) { 156 | tabLayout.addTab(tab[i]!!) 157 | } 158 | 159 | tabCount = numberOfTabs 160 | 161 | 162 | mSpreadsheetStatePagerAdapter?.notifyDataSetChanged() 163 | } 164 | 165 | 166 | } 167 | 168 | private fun setViewPagerListener(mPager: SheetViewPager) { 169 | 170 | mPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { 171 | 172 | override fun onPageSelected(position: Int) { 173 | tabSelected(position) 174 | } 175 | 176 | override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} 177 | 178 | override fun onPageScrollStateChanged(state: Int) {} 179 | }) 180 | } 181 | 182 | private fun tabSelected(position: Int) { 183 | selectedTab = position 184 | 185 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 186 | viewModel.topRow = 1 187 | viewModel.leftColumn = 1 188 | SheetLayoutManager.topRow = 1 189 | SheetLayoutManager.leftColumn = 1 190 | 191 | val spreadsheet = viewModel.spreadsheet.value 192 | 193 | spreadsheet?.workbook?.currentSheet = selectedTab 194 | 195 | val frag = mSpreadsheetStatePagerAdapter?.instantiateItem(mPager, mPager.currentItem) as SheetFragment 196 | 197 | val sheetAdapter = frag.fragmentRecyclerView.adapter as SheetAdapter 198 | 199 | 200 | sheetAdapter.notifyDataSetChanged() 201 | 202 | } 203 | 204 | 205 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 206 | when (item?.itemId) { 207 | R.id.action_jump -> { 208 | jumpDialog() 209 | return true 210 | } 211 | R.id.action_search -> { 212 | return true 213 | } 214 | R.id.action_load -> { 215 | load() 216 | return true 217 | } 218 | else -> return super.onOptionsItemSelected(item) 219 | } 220 | } 221 | 222 | fun jumpDialog() { 223 | val newFragment = JumpToCellFragment() 224 | newFragment.show(supportFragmentManager, "jump") 225 | } 226 | 227 | 228 | fun load() { 229 | 230 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 231 | viewModel.topRow = 1 232 | viewModel.leftColumn = 1 233 | SheetLayoutManager.topRow = 1 234 | SheetLayoutManager.leftColumn = 1 235 | 236 | val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 237 | addCategory(Intent.CATEGORY_OPENABLE) 238 | type = "*/*" 239 | } 240 | startActivityForResult(intent, MY_REQ_READ_EXTERNAL_STORAGE) 241 | } 242 | 243 | fun clearSelectBox() { 244 | select.text = "" 245 | } 246 | 247 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 248 | if (resultCode == Activity.RESULT_OK) { 249 | if (requestCode == MY_REQ_READ_EXTERNAL_STORAGE) { 250 | clearSelectBox() 251 | data?.data?.also { uri -> 252 | 253 | val frag = mSpreadsheetStatePagerAdapter?.instantiateItem(mPager, mPager.currentItem) as SheetFragment 254 | frag.processUri() 255 | 256 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 257 | viewModel.processUri(uri, contentResolver) 258 | } 259 | } 260 | } 261 | } 262 | 263 | 264 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 265 | 266 | menuInflater.inflate(R.menu.menu_spreadsheet, menu) 267 | 268 | val searchManager = getSystemService(Context.SEARCH_SERVICE) as SearchManager 269 | val searchView = menu?.findItem(R.id.action_search)?.getActionView() as SearchView 270 | val menuItem = menu.findItem(R.id.action_search) 271 | 272 | searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName)) 273 | 274 | searchView.setSubmitButtonEnabled(true) 275 | 276 | searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { 277 | override fun onQueryTextSubmit(s: String): Boolean { 278 | search(s) 279 | return false 280 | } 281 | 282 | override fun onQueryTextChange(s: String): Boolean { 283 | return false 284 | } 285 | }) 286 | 287 | searchView.setOnCloseListener(object : SearchView.OnCloseListener { 288 | override fun onClose(): Boolean { 289 | return false 290 | } 291 | }) 292 | 293 | menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { 294 | override fun onMenuItemActionCollapse(item: MenuItem): Boolean { 295 | lastSearch = resetLastSearch() 296 | return true 297 | } 298 | 299 | override fun onMenuItemActionExpand(item: MenuItem): Boolean { 300 | startSearch() 301 | return true 302 | } 303 | }) 304 | 305 | return true 306 | } 307 | 308 | 309 | override fun onNewIntent(intent: Intent?) { 310 | super.onNewIntent(intent) 311 | checkViewIntents(intent) 312 | } 313 | 314 | 315 | fun search(string: String) { 316 | 317 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 318 | val sheet = viewModel.spreadsheet.value 319 | 320 | sheet?.let { 321 | val searchData = SearchData(string, it, lastSearch) 322 | SearchTask(this).execute(searchData) 323 | } 324 | } 325 | 326 | fun startSearch() { 327 | 328 | val frag = mSpreadsheetStatePagerAdapter?.instantiateItem(mPager, mPager.currentItem) as SheetFragment 329 | frag.startSearch() 330 | } 331 | 332 | fun doSearchJump() { 333 | val viewModel = ViewModelProviders.of(this).get(SheetViewModel::class.java) 334 | 335 | // need to add space for column and row markers 336 | viewModel.leftColumn = lastSearch.column+1 337 | viewModel.topRow = lastSearch.row+1 338 | 339 | val fragment = mSpreadsheetStatePagerAdapter?.instantiateItem(mPager, mPager.currentItem) as SheetFragment 340 | fragment.processSearchJump() 341 | } 342 | 343 | fun resetLastSearch() : LastSearch { 344 | return LastSearch(0,0,"") 345 | } 346 | 347 | fun getSheetString() : String { 348 | return getString(R.string.sheet) 349 | } 350 | 351 | data class SearchData(val string: String, val sheet: Spreadsheet, val lastSearch: LastSearch) 352 | data class LastSearch(val row: Int, val column: Int, val word: String) 353 | 354 | inner class SpreadsheetStatePagerAdapter(fragmentManager: FragmentManager) : 355 | FragmentStatePagerAdapter(fragmentManager) { 356 | 357 | var nameList : MutableList = mutableListOf() 358 | 359 | fun setNames(list : MutableList) { 360 | nameList = list 361 | } 362 | 363 | override fun getItem(position: Int): Fragment { 364 | 365 | val fragment = SheetFragment.newInstance() 366 | 367 | /* 368 | fragment.arguments = Bundle().apply { 369 | putInt("num", position) 370 | } 371 | */ 372 | 373 | return fragment 374 | } 375 | 376 | override fun getPageTitle(position: Int): CharSequence? { 377 | 378 | val titlePos = position + 1 379 | 380 | if (nameList.size > position) { 381 | 382 | val n = nameList[position] 383 | return n 384 | } else { 385 | val sheetString = getSheetString() 386 | return "$sheetString $titlePos" 387 | } 388 | } 389 | 390 | override fun getCount(): Int { 391 | return tabCount 392 | } 393 | 394 | } 395 | 396 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/Cell.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet 2 | 3 | 4 | open class Cell { 5 | 6 | var cellValue : String = "" 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/Row.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet 2 | 3 | 4 | open class Row { 5 | 6 | var cellList : MutableList = mutableListOf() 7 | 8 | var height = 60 9 | 10 | fun getCell(column : Int) : Cell { 11 | while(cellList.size <= column) { 12 | cellList.add(Cell()) 13 | } 14 | return cellList[column] 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/Sheet.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet 2 | 3 | 4 | open class Sheet { 5 | 6 | val TOO_LARGE = 99999 7 | 8 | var rowList : MutableList = mutableListOf() 9 | 10 | var columnWidths = mutableListOf() 11 | 12 | var name : String = "" 13 | 14 | fun getNumberOfColumns() : Int { 15 | if (rowList.size == 0) return 0 16 | else return rowList[0].cellList.size 17 | } 18 | 19 | fun getRow(i : Int) : Row { 20 | 21 | // XXX: TOO_LARGE is a big number to catch bad input 22 | while (i >= rowList.size && i < TOO_LARGE) { 23 | rowList.add(Row()) 24 | } 25 | return rowList[i] 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/Spreadsheet.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet 2 | 3 | import android.util.Log 4 | import com.unwrappedapps.android.spreadsheet.spreadsheet.csv.CsvWorkbook 5 | import com.unwrappedapps.android.spreadsheet.spreadsheet.ods.OdsWorkbook 6 | import com.unwrappedapps.android.spreadsheet.spreadsheet.poi.PoiWorkbook 7 | import java.io.InputStream 8 | 9 | 10 | class Spreadsheet { 11 | 12 | var workbook : Workbook = Workbook() 13 | 14 | // empty spreadsheet 15 | constructor() { 16 | workbook = Workbook() 17 | } 18 | 19 | // spreadsheetFormat from string to other 20 | constructor(inputStream: InputStream, spreadsheetFormat: String?) { 21 | 22 | if (spreadsheetFormat.equals("csv")) { 23 | workbook = CsvWorkbook(inputStream) 24 | } 25 | else if (spreadsheetFormat.equals("xls")) { 26 | workbook = PoiWorkbook(inputStream) 27 | } 28 | else if (spreadsheetFormat.equals("xlsx")) { 29 | workbook = PoiWorkbook(inputStream) 30 | } 31 | else if (spreadsheetFormat.equals("ods")) { 32 | workbook = OdsWorkbook(inputStream) 33 | } 34 | 35 | } 36 | 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/Workbook.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet 2 | 3 | 4 | open class Workbook { 5 | 6 | var sheetList: MutableList = mutableListOf() 7 | 8 | var currentSheet : Int 9 | 10 | init { 11 | val sheet = Sheet() 12 | sheetList.add(sheet) 13 | currentSheet = 0 14 | } 15 | 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/csv/CsvCell.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.csv 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Cell 4 | 5 | 6 | class CsvCell(string: String) : Cell() { 7 | 8 | init { 9 | cellValue = string 10 | } 11 | 12 | 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/csv/CsvRow.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.csv 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Row 4 | 5 | class CsvRow(line: String) : Row() { 6 | 7 | init { 8 | 9 | var thisString = "" 10 | for (i in 0 until line.length) { 11 | val c = line[i] 12 | if (c == ',') { 13 | val csvCell = CsvCell(thisString) 14 | cellList.add(csvCell) 15 | 16 | thisString = "" 17 | } else { 18 | thisString = thisString + c 19 | } 20 | } 21 | 22 | // last one 23 | // TODO: Deal with last column with trailing ^M and such 24 | if (thisString.length > 0) { 25 | val csvCell = CsvCell(thisString) 26 | cellList.add(csvCell) 27 | } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/csv/CsvSheet.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.csv 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Sheet 4 | import java.io.InputStream 5 | 6 | class CsvSheet(inputStream: InputStream) : Sheet() { 7 | 8 | companion object { 9 | const val END_OF_STREAM = -1 10 | } 11 | 12 | init { 13 | 14 | var c: Char 15 | try { 16 | var lineSoFar = "" 17 | var nextByte = inputStream.read() 18 | while (nextByte != END_OF_STREAM) { 19 | c = nextByte.toChar() 20 | if (c == '\n') { 21 | val csvRow = CsvRow(lineSoFar) 22 | rowList.add(csvRow) 23 | lineSoFar = "" 24 | } else { 25 | lineSoFar = lineSoFar + c 26 | } 27 | nextByte = inputStream.read() 28 | } 29 | inputStream.close() 30 | } catch (e: Exception) { 31 | //Log.d("csvsheet", "error $e") 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/csv/CsvWorkbook.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.csv 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Workbook 4 | import java.io.InputStream 5 | 6 | open class CsvWorkbook (inputStream: InputStream): Workbook() { 7 | 8 | 9 | init { 10 | var csvSheet : CsvSheet 11 | 12 | csvSheet = CsvSheet(inputStream) 13 | 14 | if (sheetList.size > 1) { 15 | sheetList.clear() 16 | } 17 | sheetList.set(0, csvSheet) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/ods/OdsCell.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.ods 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Cell 4 | 5 | 6 | class OdsCell(string: String) : Cell() { 7 | 8 | init { 9 | cellValue = string 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/ods/OdsRow.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.ods 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Row 4 | 5 | 6 | class OdsRow : Row() { 7 | init { 8 | cellList = mutableListOf() 9 | } 10 | 11 | fun add(odsCell : OdsCell) { 12 | cellList.add(odsCell) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/ods/OdsSheet.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.ods 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Sheet 4 | 5 | 6 | // XXX: We can probably rename this to OdsTable if we want to 7 | class OdsSheet : Sheet() { 8 | 9 | init { 10 | rowList = mutableListOf() 11 | } 12 | 13 | fun add(odsRow: OdsRow) { 14 | rowList.add(odsRow) 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/ods/OdsWorkbook.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.ods 2 | 3 | import android.util.Log 4 | import android.util.Xml 5 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Sheet 6 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Workbook 7 | import org.xmlpull.v1.XmlPullParser 8 | import java.io.InputStream 9 | import org.xmlpull.v1.XmlPullParserException 10 | import java.io.IOException 11 | import java.util.jar.JarInputStream 12 | 13 | 14 | /* 15 | 16 | TODO: This is a very bare bones ODS implementation. 17 | Either fill it out or look for an appropriately 18 | licensed library to use for it (or make one). 19 | 20 | */ 21 | 22 | class OdsWorkbook(inputStream : InputStream) : Workbook() { 23 | 24 | 25 | init { 26 | sheetList = OdsCreator(inputStream).getList() 27 | } 28 | 29 | 30 | class OdsCreator(inputStream : InputStream) { 31 | 32 | private val ns: String? = null 33 | private var helperSheetList: MutableList = mutableListOf() 34 | 35 | init { 36 | helperSheetList = mutableListOf() 37 | makeOds(inputStream) 38 | } 39 | 40 | 41 | private fun makeOds(inputStream: InputStream) { 42 | try { 43 | val jis = JarInputStream(inputStream) 44 | var je = jis.getNextJarEntry() 45 | 46 | while (je != null) { 47 | 48 | if (je.getName().equals("content.xml")) { 49 | parse(jis) 50 | } 51 | 52 | je = jis.getNextJarEntry() 53 | 54 | } 55 | jis.close() 56 | inputStream.close() 57 | } catch (e: Exception) { 58 | Log.d("makeods", e.toString()) 59 | } 60 | 61 | 62 | } 63 | 64 | @Throws(XmlPullParserException::class, IOException::class) 65 | fun parse(inputStream: InputStream): List<*> { 66 | try { 67 | val parser = Xml.newPullParser() 68 | parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false) 69 | parser.setInput(inputStream, null) 70 | parser.nextTag() 71 | return readFeed(parser) 72 | } finally { 73 | inputStream.close() 74 | } 75 | } 76 | 77 | 78 | @Throws(XmlPullParserException::class, IOException::class) 79 | private fun readFeed(parser: XmlPullParser): List<*> { 80 | val body = mutableListOf() 81 | parser.require(XmlPullParser.START_TAG, ns, "office:document-content") 82 | while (parser.next() != XmlPullParser.END_TAG) { 83 | if (parser.eventType != XmlPullParser.START_TAG) { 84 | continue 85 | } 86 | val name = parser.name 87 | 88 | // Starts by looking for the entry tag 89 | 90 | if (name == "office:body") { 91 | body.add(readBody(parser)) // N.B. see readBody comments 92 | break 93 | } else { 94 | skip(parser) 95 | } 96 | 97 | } 98 | 99 | return body 100 | } 101 | 102 | @Throws(XmlPullParserException::class, IOException::class) 103 | private fun skip(parser: XmlPullParser) { 104 | if (parser.eventType != XmlPullParser.START_TAG) { 105 | throw IllegalStateException() 106 | } 107 | var depth = 1 108 | while (depth != 0) { 109 | when (parser.next()) { 110 | XmlPullParser.END_TAG -> depth-- 111 | XmlPullParser.START_TAG -> depth++ 112 | } 113 | } 114 | } 115 | 116 | 117 | // TODO - this function is not really returning anything 118 | // clean up later 119 | @Throws(XmlPullParserException::class, IOException::class) 120 | private fun readBody(parser: XmlPullParser) : String { 121 | 122 | parser.require(XmlPullParser.START_TAG, ns, "office:body") 123 | while (parser.next() != XmlPullParser.END_TAG) { 124 | 125 | if (parser.eventType != XmlPullParser.START_TAG) { 126 | continue 127 | } 128 | val name = parser.name 129 | 130 | if (name == "office:spreadsheet") { 131 | readSpreadsheet(parser) 132 | break 133 | } else { 134 | skip(parser) 135 | } 136 | 137 | } 138 | return "filler" // N.B. - bogus body return 139 | } 140 | 141 | @Throws(XmlPullParserException::class, IOException::class) 142 | private fun readSpreadsheet(parser: XmlPullParser) { 143 | 144 | parser.require(XmlPullParser.START_TAG, ns, "office:spreadsheet") 145 | 146 | var pn = parser.next() 147 | 148 | while (pn != XmlPullParser.END_TAG) { 149 | 150 | if (parser.eventType != XmlPullParser.START_TAG) { 151 | continue 152 | } 153 | val name = parser.name 154 | 155 | val odsTable: OdsSheet 156 | 157 | if (name == "table:table") { 158 | 159 | val s = checkForAttribute(parser, "table:name") 160 | 161 | odsTable = readTable(parser) 162 | 163 | if (s.length > 0) { 164 | odsTable.name = s 165 | } 166 | 167 | helperSheetList.add(odsTable) 168 | 169 | 170 | } else { 171 | skip(parser) 172 | } 173 | 174 | pn = parser.next() 175 | 176 | } 177 | 178 | } 179 | 180 | // table or sheet 181 | @Throws(XmlPullParserException::class, IOException::class) 182 | private fun readTable(parser: XmlPullParser): OdsSheet { 183 | 184 | val odsTable = OdsSheet() 185 | var odsRow: OdsRow 186 | 187 | parser.require(XmlPullParser.START_TAG, ns, "table:table") 188 | while (parser.next() != XmlPullParser.END_TAG) { 189 | if (parser.eventType != XmlPullParser.START_TAG) { 190 | continue 191 | } 192 | val name = parser.name 193 | 194 | if (name == "table:table-row") { 195 | 196 | val repeatNum = checkForRepeats(parser, "table:number-rows-repeated") 197 | 198 | odsRow = readTableRow(parser) 199 | odsTable.add(odsRow) 200 | if (repeatNum > 0) { 201 | for (i in 1 until repeatNum) { 202 | odsTable.add(odsRow) 203 | } 204 | } 205 | 206 | 207 | } else if (name == "table:table-header-rows") { 208 | odsRow = readTableHeaderRows(parser) 209 | odsTable.add(odsRow) 210 | } else { 211 | skip(parser) 212 | } 213 | } 214 | return odsTable 215 | } 216 | 217 | @Throws(XmlPullParserException::class, IOException::class) 218 | private fun readTableHeaderRows(parser: XmlPullParser): OdsRow { 219 | 220 | parser.require(XmlPullParser.START_TAG, ns, "table:table-header-rows") 221 | 222 | while (parser.next() != XmlPullParser.END_TAG) { 223 | if (parser.eventType != XmlPullParser.START_TAG) { 224 | continue 225 | } 226 | val name = parser.name 227 | 228 | if (name == "table:table-row") { 229 | 230 | return readTableRow(parser) 231 | } else { 232 | skip(parser) 233 | } 234 | } 235 | return OdsRow() 236 | } 237 | 238 | 239 | @Throws(XmlPullParserException::class, IOException::class) 240 | private fun readTableRow(parser: XmlPullParser): OdsRow { 241 | 242 | val odsRow = OdsRow() 243 | var odsCell: OdsCell 244 | 245 | parser.require(XmlPullParser.START_TAG, ns, "table:table-row") 246 | while (parser.next() != XmlPullParser.END_TAG) { 247 | if (parser.eventType != XmlPullParser.START_TAG) { 248 | continue 249 | } 250 | val name = parser.name 251 | 252 | if (name == "table:table-cell") { 253 | 254 | val repeatNum = checkForRepeats(parser, "table:number-columns-repeated") 255 | 256 | odsCell = readTableCell(parser) 257 | odsRow.add(odsCell) 258 | 259 | if (repeatNum > 0) { 260 | for (i in 1 until repeatNum) { 261 | odsRow.add(odsCell) 262 | } 263 | } 264 | 265 | } else { 266 | skip(parser) 267 | } 268 | } 269 | return odsRow 270 | 271 | } 272 | 273 | fun checkForRepeats(parser: XmlPullParser, attribute: String): Int { 274 | 275 | val r = checkForAttribute(parser, attribute) 276 | return if (r.length > 0) { 277 | r.toInt() 278 | } else { 279 | -1 280 | } 281 | } 282 | 283 | fun checkForAttribute(parser: XmlPullParser, attribute: String): String { 284 | val count = parser.attributeCount 285 | var r = "" 286 | for (i in 0 until count) { 287 | val names = parser.getAttributeName(i) 288 | if (names == attribute) { 289 | r = parser.getAttributeValue(i) 290 | } 291 | } 292 | return r 293 | 294 | } 295 | 296 | 297 | @Throws(XmlPullParserException::class, IOException::class) 298 | private fun readTableCell(parser: XmlPullParser): OdsCell { 299 | 300 | var textP = "" 301 | 302 | parser.require(XmlPullParser.START_TAG, ns, "table:table-cell") 303 | 304 | while (parser.next() != XmlPullParser.END_TAG) { 305 | 306 | if (parser.eventType != XmlPullParser.START_TAG) { 307 | continue 308 | } 309 | val name = parser.name 310 | 311 | if (name == "text:p") { 312 | textP = readTextP(parser) 313 | } else { 314 | skip(parser) 315 | } 316 | } 317 | 318 | return OdsCell(textP) 319 | } 320 | 321 | @Throws(XmlPullParserException::class, IOException::class) 322 | private fun readTextP(parser: XmlPullParser): String { 323 | 324 | parser.require(XmlPullParser.START_TAG, ns, "text:p") 325 | 326 | var s = "" 327 | 328 | while (parser.next() != XmlPullParser.END_TAG) { 329 | if (parser.eventType == 4) { 330 | 331 | // TODO: Doing first one, handle others in future 332 | if (s.length < 1) { 333 | s = parser.text 334 | } 335 | 336 | } else { 337 | skip(parser) 338 | } 339 | } 340 | 341 | return s 342 | } 343 | 344 | 345 | fun getList(): MutableList { 346 | 347 | return helperSheetList 348 | } 349 | } 350 | 351 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/poi/PoiCell.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.poi 2 | 3 | import org.apache.poi.ss.usermodel.CellType 4 | import org.apache.poi.ss.usermodel.DateUtil 5 | 6 | class PoiCell() : com.unwrappedapps.android.spreadsheet.spreadsheet.Cell() { 7 | 8 | lateinit var pCell: org.apache.poi.ss.usermodel.Cell 9 | 10 | constructor(cell: org.apache.poi.ss.usermodel.Cell) : this() { 11 | 12 | pCell = cell 13 | val value : String? 14 | 15 | when (cell.cellTypeEnum) { 16 | 17 | CellType.FORMULA -> value = cell.cellFormula 18 | 19 | CellType.NUMERIC -> if (DateUtil.isCellDateFormatted(cell)) 20 | value = "" + cell.dateCellValue 21 | else 22 | value = "" + cell.numericCellValue 23 | //CellType.STRING 24 | else -> value = cell.stringCellValue 25 | } 26 | 27 | if (value != null) { 28 | cellValue = value 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/poi/PoiRow.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.poi 2 | 3 | 4 | class PoiRow() : com.unwrappedapps.android.spreadsheet.spreadsheet.Row() { 5 | 6 | val magicHeightDiv = 10 7 | 8 | lateinit var pRow : org.apache.poi.ss.usermodel.Row 9 | 10 | constructor(row: org.apache.poi.ss.usermodel.Row) : this() { 11 | pRow = row 12 | 13 | height = pRow.height / magicHeightDiv 14 | 15 | //val numberOfCells = pRow.physicalNumberOfCells 16 | val numberOfCells = pRow.lastCellNum 17 | 18 | for (i in 0..numberOfCells-1) { 19 | val pCell = pRow.getCell(i) 20 | // TODO: decide if should be skipped or a blank one 21 | 22 | if (pCell != null) { 23 | val cell = PoiCell(pCell) 24 | cellList.add(cell) 25 | } 26 | } 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/poi/PoiSheet.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.poi 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Sheet as MySheet 4 | 5 | class PoiSheet() : com.unwrappedapps.android.spreadsheet.spreadsheet.Sheet() { 6 | 7 | lateinit var pSheet : org.apache.poi.ss.usermodel.Sheet 8 | constructor(sheet: org.apache.poi.ss.usermodel.Sheet) : this() { 9 | pSheet = sheet 10 | 11 | name = pSheet.sheetName 12 | 13 | val numberOfRows = sheet.physicalNumberOfRows 14 | 15 | // XXX: should this not match the row height divisor? 16 | //val magicWidthDiv = 10 17 | val magicWidthDiv = 15 18 | 19 | var max : Short = 0 20 | var currentMax : Short 21 | 22 | for (i in 0 until numberOfRows) { 23 | val pRow = pSheet.getRow(i) 24 | 25 | // TODO: decide if should be skipped or a blank one 26 | if (pRow != null) { 27 | val row = PoiRow(pRow) 28 | rowList.add(row) 29 | currentMax = pRow.lastCellNum 30 | if (currentMax > max) max = currentMax 31 | } 32 | } 33 | 34 | for (i in 0 until max) { 35 | columnWidths.add(pSheet.getColumnWidth(i)/magicWidthDiv) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/spreadsheet/poi/PoiWorkbook.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.spreadsheet.poi 2 | 3 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Workbook 4 | import org.apache.poi.ss.usermodel.WorkbookFactory 5 | import java.io.InputStream 6 | 7 | open class PoiWorkbook (inputStream: InputStream): Workbook() { 8 | 9 | init { 10 | // hssf/xssf 11 | val workbook : org.apache.poi.ss.usermodel.Workbook = WorkbookFactory.create(inputStream) 12 | 13 | sheetList.clear() 14 | 15 | for (i in 0 until workbook.numberOfSheets) { 16 | val poiSheet = workbook.getSheetAt(i) 17 | val pSheet : PoiSheet 18 | pSheet = PoiSheet(poiSheet) 19 | sheetList.add(pSheet) 20 | } 21 | } 22 | 23 | companion object { 24 | init { 25 | System.setProperty( 26 | "org.apache.poi.javax.xml.stream.XMLInputFactory", 27 | "com.fasterxml.aalto.stax.InputFactoryImpl" 28 | ) 29 | System.setProperty( 30 | "org.apache.poi.javax.xml.stream.XMLOutputFactory", 31 | "com.fasterxml.aalto.stax.OutputFactoryImpl" 32 | ) 33 | System.setProperty( 34 | "org.apache.poi.javax.xml.stream.XMLEventFactory", 35 | "com.fasterxml.aalto.stax.EventFactoryImpl" 36 | ) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/JumpToCellFragment.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import androidx.fragment.app.DialogFragment 6 | import android.content.DialogInterface 7 | import android.widget.EditText 8 | import androidx.appcompat.app.AlertDialog 9 | import androidx.lifecycle.ViewModelProviders 10 | import com.unwrappedapps.android.spreadsheet.R 11 | 12 | 13 | class JumpToCellFragment : DialogFragment() { 14 | 15 | 16 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 17 | 18 | // Use the Builder class for convenient dialog construction 19 | //val builder = AlertDialog.Builder(activity) 20 | 21 | val dialogContext = context 22 | 23 | if (dialogContext == null) { 24 | return Dialog(dialogContext) 25 | } 26 | 27 | val builder = AlertDialog.Builder(dialogContext) 28 | 29 | val inflater = activity?.layoutInflater 30 | 31 | val view = inflater?.inflate(R.layout.jump_dialog, null) 32 | 33 | val editText = view?.findViewById(R.id.cell) as EditText 34 | 35 | val viewModel = activity?.run { 36 | ViewModelProviders.of(this).get(SheetViewModel::class.java) 37 | } 38 | 39 | builder.setView(view) 40 | builder.setMessage(R.string.jump_message) 41 | 42 | var cell : String 43 | 44 | builder 45 | .setPositiveButton(R.string.jump_go, DialogInterface.OnClickListener { dialog, id -> 46 | 47 | cell = editText.text.toString() 48 | 49 | val regex: Regex = Regex("^[A-Za-z]*[0-9]*") 50 | 51 | if (cell.length > 0 && cell.matches(regex)) { // somewhat matches 52 | viewModel?.setTheJumpCell(cell) 53 | } 54 | }) 55 | 56 | .setNegativeButton(R.string.jump_cancel, DialogInterface.OnClickListener { dialog, id -> 57 | // User cancelled the dialog 58 | //Log.d("neg is", "n") 59 | }) 60 | 61 | // Create the AlertDialog object and return it 62 | return builder.create() 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/SheetAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import android.graphics.Color 4 | import android.graphics.drawable.ColorDrawable 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.recyclerview.widget.RecyclerView 8 | import android.view.LayoutInflater 9 | import android.widget.TextView 10 | import com.unwrappedapps.android.spreadsheet.R 11 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Spreadsheet 12 | import android.view.Gravity 13 | 14 | 15 | class SheetAdapter(val density: Int, var select: TextView?) : 16 | RecyclerView.Adapter() { 17 | 18 | 19 | companion object { 20 | private var spreadsheet: Spreadsheet? = null 21 | 22 | private const val CELL = 3 23 | private const val COLUMN_MARKER = 2 24 | private const val ROW_MARKER = 1 25 | 26 | //30 can do 3 digits 27 | //36 can do 4 digits 28 | //private val ROW_MARKER_WIDTH = 45 29 | private const val ROW_MARKER_WIDTH = 50 30 | 31 | private const val ROW_HEIGHT = 60 32 | } 33 | 34 | init { 35 | select?.text = "" 36 | } 37 | 38 | fun assignSpreadsheet(ss : Spreadsheet?) { 39 | spreadsheet = ss 40 | } 41 | 42 | override fun getItemViewType(position: Int): Int { 43 | 44 | val coordinate = posToMarkers(position) 45 | 46 | return if (coordinate.r == 0) { 47 | COLUMN_MARKER 48 | } else if (coordinate.c == 0) { 49 | ROW_MARKER 50 | } else { 51 | CELL 52 | } 53 | 54 | } 55 | 56 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 57 | 58 | // Create a new view. 59 | // could change markers by viewType here if necessary 60 | val view: View 61 | 62 | view = LayoutInflater.from(parent.getContext()) 63 | .inflate(com.unwrappedapps.android.spreadsheet.R.layout.graph_cell, parent, false) 64 | 65 | return ViewHolder(view) 66 | } 67 | 68 | override fun getItemCount(): Int { 69 | var items: Int 70 | 71 | items = 2147483647 // biggest int I can make 72 | items-- 73 | items-- 74 | return items 75 | } 76 | 77 | // what to do when position is bound to viewholder 78 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 79 | 80 | if (getItemViewType(position) == CELL) { 81 | makeCell(holder, position) 82 | } else if (getItemViewType(position) == COLUMN_MARKER) { 83 | makeColumnMarker(holder, position) 84 | } else { 85 | makeRowMarker(holder, position) 86 | } 87 | 88 | } 89 | 90 | 91 | private fun makeCell(viewHolder: ViewHolder, position: Int) { 92 | 93 | val workbook = spreadsheet?.workbook 94 | val sheet = workbook?.sheetList?.get(workbook.currentSheet) 95 | 96 | val numColumns = sheet?.getNumberOfColumns() 97 | 98 | val coordinate = posToMarkers(position) 99 | var r = coordinate.r 100 | var c = coordinate.c 101 | 102 | r-- 103 | c-- 104 | 105 | val cellsValue: String? 106 | 107 | if (numColumns == null) return 108 | 109 | if (numColumns < 1) { 110 | cellsValue = " " 111 | } else { 112 | cellsValue = sheet.getRow(r).getCell(c).cellValue 113 | } 114 | 115 | viewHolder.textView.text = cellsValue 116 | 117 | if (sheet.columnWidths.size > c) { 118 | val baseWidth = sheet.columnWidths[c] 119 | val denseWidth = baseWidth * density 120 | viewHolder.textView.width = denseWidth 121 | } else { 122 | viewHolder.textView.width = ROW_MARKER_WIDTH*density*2 123 | } 124 | 125 | val denseHeight = density * sheet.getRow(r).height 126 | 127 | viewHolder.textView.height = denseHeight 128 | 129 | //val alpha = 0; 130 | val alpha = 255 131 | 132 | val sdk = android.os.Build.VERSION.SDK_INT 133 | if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { 134 | viewHolder.textView.setBackgroundDrawable(ColorDrawable(Color.argb(alpha, 255, 255, 255))) 135 | 136 | } else { 137 | viewHolder.textView.setBackground(ColorDrawable(Color.argb(alpha, 255, 255, 255))) 138 | } 139 | 140 | viewHolder.textView.setOnClickListener( 141 | { 142 | select?.text = cellsValue 143 | }) 144 | } 145 | 146 | 147 | private fun makeRowMarker(viewHolder: ViewHolder, position: Int) { 148 | 149 | var (row,_) = posToMarkers(position) 150 | 151 | row-- 152 | val workbook = spreadsheet?.workbook 153 | val sheet = workbook?.sheetList?.get(workbook.currentSheet) 154 | val height = sheet?.getRow(row)?.height ?: ROW_HEIGHT 155 | val denseHeight = density * height 156 | 157 | viewHolder.textView.height = denseHeight 158 | setBackground(viewHolder) 159 | 160 | val rm = Integer.toString(row+1) 161 | 162 | val width = ROW_MARKER_WIDTH * density 163 | 164 | viewHolder.textView.height = density * height 165 | viewHolder.textView.setWidth(width) 166 | viewHolder.textView.setText(rm) 167 | viewHolder.textView.setGravity(Gravity.CENTER) 168 | } 169 | 170 | 171 | // must be wide enough for 2-3 letter column markers 172 | private fun makeColumnMarker(viewHolder: ViewHolder, position: Int) { 173 | 174 | val (_, c) = posToMarkers(position) 175 | 176 | val workbook = spreadsheet?.workbook 177 | val sheet = workbook?.sheetList?.get(workbook.currentSheet) 178 | 179 | val size = sheet?.columnWidths?.size 180 | 181 | if (position == 0) { 182 | viewHolder.textView.width = ROW_MARKER_WIDTH * density 183 | 184 | } else { 185 | if (size != null) { 186 | if (size >= c) { 187 | val baseWidth = sheet.columnWidths[c-1] 188 | val denseWidth = baseWidth * density 189 | viewHolder.textView.width = denseWidth 190 | } else { 191 | viewHolder.textView.width = ROW_MARKER_WIDTH * density * 2 192 | } 193 | } else { 194 | viewHolder.textView.width = ROW_MARKER_WIDTH * density * 2 195 | } 196 | } 197 | 198 | setBackground(viewHolder) 199 | 200 | val lastPosition = (c - 1) % 26 201 | 202 | var text = getLetter(lastPosition) 203 | 204 | if (c > 26) { 205 | var before = (c - 1) / 26 206 | before-- 207 | 208 | if (before >= 26) { 209 | before = before % 26 210 | } 211 | 212 | val beforeLast = getLetter(before) 213 | text = beforeLast + text 214 | 215 | } else if (position == 0) { 216 | text = " " 217 | } 218 | 219 | 220 | if (c > 26 * (26 + 1)) { 221 | var ch = c 222 | ch = ch - 26 223 | ch = ch - 1 224 | ch = ch / 26 225 | ch = ch / 26 226 | ch = ch - 1 227 | 228 | val beforebeforeLast = getLetter(ch) 229 | text = beforebeforeLast + text 230 | 231 | } else if (position == 0) { 232 | text = " " 233 | } 234 | 235 | viewHolder.textView.setGravity(Gravity.CENTER) 236 | viewHolder.textView.setText(text) 237 | } 238 | 239 | 240 | private fun setBackground(viewHolder: ViewHolder) { 241 | 242 | //val gray = 84 243 | val gray = 193 244 | 245 | val sdk = android.os.Build.VERSION.SDK_INT 246 | if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) { 247 | //setBackgroundDrawable(); 248 | viewHolder.textView.setBackgroundDrawable(ColorDrawable(Color.argb(255, gray,gray,gray))) 249 | 250 | } else { 251 | viewHolder.textView.setBackground(ColorDrawable(Color.argb(255, gray,gray,gray))) 252 | 253 | } 254 | } 255 | 256 | private fun getLetter(lastPosition: Int): String { 257 | 258 | val text: String 259 | 260 | when (lastPosition) { 261 | 0 -> text = "A" 262 | 1 -> text = "B" 263 | 2 -> text = "C" 264 | 3 -> text = "D" 265 | 4 -> text = "E" 266 | 5 -> text = "F" 267 | 6 -> text = "G" 268 | 7 -> text = "H" 269 | 8 -> text = "I" 270 | 9 -> text = "J" 271 | 10 -> text = "K" 272 | 11 -> text = "L" 273 | 12 -> text = "M" 274 | 13 -> text = "N" 275 | 14 -> text = "O" 276 | 15 -> text = "P" 277 | 16 -> text = "Q" 278 | 17 -> text = "R" 279 | 18 -> text = "S" 280 | 19 -> text = "T" 281 | 20 -> text = "U" 282 | 21 -> text = "V" 283 | 22 -> text = "W" 284 | 23 -> text = "X" 285 | 24 -> text = "Y" 286 | 25 -> text = "Z" 287 | else -> text = " " 288 | } 289 | return text 290 | } 291 | 292 | 293 | // translate position to column and row 294 | private fun posToMarkers(pos: Int): Coordinate { 295 | val diff: Int 296 | val column: Int 297 | 298 | var p = pos 299 | p++ 300 | p = p * 2 301 | val r = Math.floor(Math.sqrt(p.toDouble())) 302 | val w = r.toInt() 303 | 304 | val tryit = (w + 1) * w / 2 305 | 306 | if (tryit > pos) { 307 | val low = (w - 1) * w / 2 308 | diff = low - pos 309 | column = w - 1 + diff 310 | } else { 311 | diff = tryit - pos 312 | column = w + diff 313 | } 314 | 315 | val row = diff * -1 316 | 317 | return Coordinate(row, column) 318 | } 319 | 320 | 321 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 322 | val textView: TextView 323 | 324 | init { 325 | textView = view.findViewById(R.id.textView) 326 | } 327 | } 328 | 329 | private data class Coordinate(val r : Int, val c : Int) 330 | 331 | } 332 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/SheetFragment.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import androidx.lifecycle.ViewModelProviders 4 | import android.os.Bundle 5 | import android.util.DisplayMetrics 6 | import androidx.fragment.app.Fragment 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import androidx.lifecycle.Observer 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.unwrappedapps.android.spreadsheet.R 13 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Spreadsheet 14 | import kotlinx.android.synthetic.main.sheet_fragment.view.* 15 | 16 | /* 17 | 18 | TODO: when screen rotates in 2nd, 3rd, 4th etc. sheet, have the 19 | left top cell remain the same (do not reset to A1) 20 | 21 | 22 | */ 23 | 24 | class SheetFragment : Fragment() { 25 | 26 | lateinit var fragmentRecyclerView : RecyclerView 27 | 28 | companion object { 29 | fun newInstance() = SheetFragment() 30 | } 31 | 32 | private var viewModel: SheetViewModel? = null 33 | 34 | fun startSearch() { 35 | val sheetLayoutManager = fragmentRecyclerView.layoutManager as SheetLayoutManager 36 | sheetLayoutManager.startSearch() 37 | } 38 | 39 | override fun onResume() { 40 | super.onResume() 41 | val sheetLayoutManager = fragmentRecyclerView.layoutManager as SheetLayoutManager 42 | sheetLayoutManager.removeAllViews() 43 | fragmentRecyclerView.adapter?.notifyDataSetChanged() 44 | sheetLayoutManager.resetLayoutManagerSearch() 45 | } 46 | 47 | fun processSearchJump() { 48 | jumpToNewCoordinates().resetLayoutManagerSearch() 49 | } 50 | 51 | override fun onCreateView( 52 | inflater: LayoutInflater, container: ViewGroup?, 53 | savedInstanceState: Bundle? 54 | ): View { 55 | val view = inflater.inflate(R.layout.sheet_fragment, container, false) 56 | val recyclerView = view.recyclerview 57 | 58 | // use this setting to improve performance if you know that changes 59 | // in content do not change the layout size of the RecyclerView 60 | recyclerView.setHasFixedSize(true) 61 | 62 | val displayMetrics = DisplayMetrics() 63 | activity?.windowManager?.defaultDisplay?.getMetrics(displayMetrics) 64 | 65 | val density = displayMetrics.density.toInt() 66 | 67 | val sheetAdapter = SheetAdapter(density, activity?.findViewById(R.id.select)) 68 | 69 | sheetAdapter.assignSpreadsheet(Spreadsheet()) 70 | 71 | recyclerView.adapter = sheetAdapter 72 | 73 | val sheetLayoutManager = SheetLayoutManager() 74 | recyclerView.layoutManager = sheetLayoutManager 75 | 76 | fragmentRecyclerView = recyclerView 77 | 78 | return view 79 | } 80 | 81 | override fun onActivityCreated(savedInstanceState: Bundle?) { 82 | super.onActivityCreated(savedInstanceState) 83 | 84 | viewModel = activity?.run { 85 | ViewModelProviders.of(this).get(SheetViewModel::class.java) 86 | } 87 | 88 | val topRow = viewModel?.topRow 89 | val leftColumn = viewModel?.leftColumn 90 | 91 | if (topRow != null) { 92 | SheetLayoutManager.topRow = topRow 93 | } 94 | 95 | if (leftColumn != null) { 96 | SheetLayoutManager.leftColumn = leftColumn 97 | } 98 | 99 | viewModel?.sheetLoadState?.observe(this , Observer { 100 | val sheetAdapter = fragmentRecyclerView.adapter as SheetAdapter 101 | sheetAdapter.assignSpreadsheet(viewModel?.spreadsheet?.value) 102 | sheetAdapter.notifyDataSetChanged() 103 | }) 104 | 105 | viewModel?.jumpCell?.observe(this, Observer { item -> 106 | jumpToNewCoordinates() 107 | }) 108 | } 109 | 110 | 111 | fun jumpToNewCoordinates() : SheetLayoutManager { 112 | val tr = viewModel?.topRow 113 | val lc = viewModel?.leftColumn 114 | 115 | if (tr != null && lc != null) { 116 | SheetLayoutManager.topRow = tr 117 | SheetLayoutManager.leftColumn = lc 118 | } 119 | 120 | val sheetAdapter = fragmentRecyclerView.adapter as SheetAdapter 121 | sheetAdapter.notifyDataSetChanged() 122 | 123 | val sheetLayoutManager = fragmentRecyclerView.layoutManager as SheetLayoutManager 124 | sheetLayoutManager.removeAllViews() 125 | return sheetLayoutManager 126 | 127 | } 128 | 129 | fun processUri() { 130 | val sheetLayoutManager = fragmentRecyclerView.layoutManager as SheetLayoutManager 131 | sheetLayoutManager.resetToTopLeft() 132 | } 133 | 134 | override fun onDestroy() { 135 | super.onDestroy() 136 | 137 | viewModel?.topRow = SheetLayoutManager.topRow 138 | viewModel?.leftColumn = SheetLayoutManager.leftColumn 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/SheetLayoutManager.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import android.view.View 4 | import android.view.ViewGroup 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class SheetLayoutManager : RecyclerView.LayoutManager() { 8 | 9 | companion object { 10 | // need to know if we should stop scrolling up and left, to begin with 11 | var leftColumn = 1 12 | var topRow = 1 13 | 14 | private const val HEADER_COLUMN: Int = 0 15 | private const val HEADER_ROW: Int = 0 16 | private const val CELL : String = "cell" 17 | private const val ROW : String = "row" 18 | private const val COLUMN : String = "column" 19 | private const val TOP_LEFT : String = "topLeft" 20 | 21 | private var skipLayoutForSearch : Boolean = false 22 | 23 | enum class Tilt{ VERTICAL, HORIZONTAL } 24 | enum class Side{ ROWS, COLUMNS } 25 | enum class Direction{ LIMITED, UNLIMITED } 26 | } 27 | 28 | override fun onLayoutChildren(recycler: RecyclerView.Recycler?, 29 | state: RecyclerView.State?) { 30 | doInitialLayout(recycler) 31 | } 32 | 33 | override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int { 34 | handleScroll(dx, recycler, Tilt.HORIZONTAL) 35 | return dx 36 | } 37 | 38 | override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler?, state: RecyclerView.State?): Int { 39 | handleScroll(dy, recycler, Tilt.VERTICAL) 40 | return dy 41 | } 42 | 43 | override fun canScrollHorizontally() = true 44 | 45 | override fun canScrollVertically() = true 46 | 47 | override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { 48 | return LayoutParams( 49 | ViewGroup.LayoutParams.WRAP_CONTENT, 50 | ViewGroup.LayoutParams.WRAP_CONTENT) 51 | } 52 | 53 | 54 | // mark when search done to prevent onLayoutChildren call 55 | // this may need to be made more complex than a boolean 56 | fun startSearch() { 57 | skipLayoutForSearch = true 58 | } 59 | 60 | fun resetLayoutManagerSearch() { 61 | skipLayoutForSearch = false 62 | } 63 | 64 | fun resetToTopLeft() { 65 | leftColumn = 1 66 | topRow = 1 67 | } 68 | 69 | private fun doInitialLayout(recycler: RecyclerView.Recycler?) { 70 | 71 | if (skipLayoutForSearch) { 72 | return 73 | } 74 | 75 | val jumpColumn = leftColumn 76 | val jumpRow = topRow 77 | 78 | var currentColumn = HEADER_COLUMN 79 | var currentRow = HEADER_ROW 80 | var cellView : View? 81 | var filledWidth = 0 82 | var filledHeight = 0 83 | var cellHeight = 0 84 | var columnInc = 0 85 | var rowInc = 0 86 | 87 | if (recycler != null) { 88 | removeAndRecycleAllViews(recycler) 89 | detachAndScrapAttachedViews(recycler) 90 | } 91 | 92 | while (height - filledHeight >= 0) { 93 | while (width - filledWidth >= 0) { 94 | cellView = layoutChildView(recycler, currentColumn, currentRow, filledWidth, filledHeight, Direction.UNLIMITED, Tilt.VERTICAL) 95 | filledWidth = filledWidth + getDecoratedSide(cellView, Tilt.VERTICAL) 96 | currentColumn = jumpColumn + columnInc++ 97 | cellHeight = getDecoratedSide(cellView, Tilt.HORIZONTAL) // we don't need to measure this each time 98 | } 99 | 100 | currentColumn = HEADER_COLUMN 101 | columnInc = 0 102 | filledWidth = 0 103 | filledHeight = filledHeight + cellHeight 104 | currentRow = jumpRow + rowInc++ 105 | } 106 | } 107 | 108 | 109 | // TODO: think of more elegant solution 110 | private fun getDecoratedMeasuredNullableWidth (cellView : View?) : Int = 111 | if (cellView == null) 0 112 | else getDecoratedMeasuredWidth(cellView) 113 | 114 | // TODO: think of more elegant solution 115 | private fun getDecoratedMeasuredNullableHeight (cellView : View?) : Int = 116 | if (cellView == null) 0 117 | else getDecoratedMeasuredHeight(cellView) 118 | 119 | 120 | private fun layoutChildView(recycler: RecyclerView.Recycler?, 121 | column: Int, row: Int, 122 | fWidth: Int, fHeight: Int, 123 | direction: Direction, tilt: Tilt): View? { 124 | 125 | val position = markerToPos(row, column) 126 | val cellView = recycler?.getViewForPosition(position) ?: return null 127 | 128 | // TODO: This call is needed for a newly loaded file, not for a rotation etc. 129 | // so it can be called less 130 | recycler.bindViewToPosition(cellView,position) 131 | 132 | addView(cellView) 133 | measureChildWithMargins(cellView, 0, 0) 134 | val cellWidth = getDecoratedMeasuredWidth(cellView) 135 | val cellHeight = getDecoratedMeasuredHeight(cellView) 136 | 137 | when { 138 | direction == Direction.UNLIMITED -> 139 | layoutDecorated(cellView, fWidth, fHeight, cellWidth + fWidth, cellHeight + fHeight) 140 | tilt == Tilt.VERTICAL -> 141 | layoutDecorated(cellView, fWidth, fHeight - cellHeight, cellWidth + fWidth, fHeight) 142 | else -> // LIMITED HORIZONTAL (right to left) 143 | layoutDecorated(cellView, fWidth - cellWidth, fHeight, fWidth, cellHeight + fHeight) 144 | } 145 | 146 | setCellType(cellView, row, column) 147 | 148 | return cellView 149 | } 150 | 151 | 152 | private fun markerToPos(r: Int, c: Int): Int { 153 | val t = c + r + 1 154 | return t * (t + 1) / 2 - c - 1 155 | } 156 | 157 | // row and column headers get scrolled under, top left header scrolled under by all 158 | private fun setCellType(cellView: View?, row: Int, column: Int) { 159 | if (row == 0 || column == 0) { 160 | if (row == 0 && column == 0) { 161 | cellView?.z = 1.2f 162 | cellView?.tag = TOP_LEFT 163 | } else { 164 | cellView?.z = 1.1f 165 | if (column == 0) { 166 | cellView?.tag = COLUMN 167 | } 168 | if (row == 0) { 169 | cellView?.tag = ROW 170 | } 171 | } 172 | } else { 173 | cellView?.tag = CELL 174 | } 175 | } 176 | 177 | 178 | private fun handleScroll(d: Int, recycler: RecyclerView.Recycler?, tilt : Tilt) { 179 | 180 | val direction : Direction 181 | var dd : Int 182 | 183 | direction = if (d < 0) Direction.LIMITED else Direction.UNLIMITED 184 | 185 | if (direction == Direction.LIMITED) { 186 | dd=d*-1 187 | do { 188 | dd = processLimitedScroll(dd, recycler, tilt) 189 | scrapOffscreenViews(recycler, direction, tilt) 190 | } while (dd > 0 && ((topRow > 1 && tilt == Tilt.VERTICAL) || (leftColumn > 1 && tilt == Tilt.HORIZONTAL))) 191 | } else { 192 | dd=d 193 | do { 194 | dd = processUnlimitedScroll(dd, recycler, tilt) 195 | scrapOffscreenViews(recycler, direction, tilt) 196 | } while (dd > 0) 197 | } 198 | } 199 | 200 | 201 | // unlimited - do not need to worry about scrolling past row 1, column A row/column markers 202 | private fun processUnlimitedScroll(scrollSize: Int, recycler: RecyclerView.Recycler?, tilt: Tilt) : Int { 203 | 204 | val orientation = if (tilt == Tilt.VERTICAL) COLUMN else ROW 205 | 206 | val farthestOut = getFarthest(tilt) 207 | 208 | val breadth = if (tilt == Tilt.VERTICAL) height else width 209 | val amountBeyondScreen = farthestOut - breadth 210 | 211 | if (amountBeyondScreen > scrollSize) { 212 | offsetCells(scrollSize, orientation) 213 | } else { 214 | offsetCells(amountBeyondScreen, orientation) 215 | newMarker(recycler, Direction.UNLIMITED, tilt) 216 | var remainingMove = scrollSize - amountBeyondScreen 217 | return remainingMove 218 | } 219 | return 0 220 | } 221 | 222 | // limited - need to worry about scrolling past row 1, column A row/column markers 223 | private fun processLimitedScroll(scrollSize: Int, recycler: RecyclerView.Recycler?, tilt : Tilt) : Int { 224 | 225 | val orientation = if (tilt == Tilt.VERTICAL) COLUMN else ROW 226 | 227 | val ltd = getLimited(tilt) 228 | 229 | if (ltd >= scrollSize) { // or > 230 | offsetCells(-scrollSize, orientation) 231 | } else if (topRow <= 1 && tilt == Tilt.VERTICAL || leftColumn <= 1 && tilt == Tilt.HORIZONTAL) { 232 | offsetCells(-ltd, orientation) 233 | } else { 234 | val rowSide = newMarker(recycler, Direction.LIMITED, tilt) 235 | val remainSize = rowSide - scrollSize 236 | return remainSize*-1 237 | } 238 | 239 | return 0 240 | } 241 | 242 | 243 | private fun newMarker(recycler: RecyclerView.Recycler?, direction: Direction, tilt: Tilt): Int { 244 | 245 | val screenBreadth: Int 246 | val screenLimit: Int 247 | val cellView: View? 248 | val currentColumn: Int 249 | val currentRow: Int 250 | 251 | if (tilt == Tilt.VERTICAL) { 252 | 253 | if (direction == Direction.LIMITED) { 254 | topRow-- 255 | currentRow = topRow 256 | screenLimit = getSmallest(tilt) 257 | 258 | } else { 259 | currentRow = getSideCount(Side.ROWS) + topRow 260 | screenLimit = height 261 | } 262 | 263 | currentColumn = leftColumn 264 | screenBreadth = width 265 | cellView = layoutChildView(recycler, 0, currentRow, 0, screenLimit, direction, tilt) 266 | 267 | } else { 268 | 269 | if (direction == Direction.LIMITED) { 270 | leftColumn-- 271 | currentColumn = leftColumn 272 | screenLimit = getSmallest(tilt) 273 | } else { 274 | currentColumn = getSideCount(Side.COLUMNS) + leftColumn 275 | screenLimit = width 276 | } 277 | 278 | currentRow = topRow 279 | screenBreadth = height 280 | cellView = layoutChildView(recycler, currentColumn, 0, screenLimit, 0, direction, tilt) 281 | } 282 | 283 | val perpendicularTilt = if (tilt == Tilt.VERTICAL) Tilt.HORIZONTAL else Tilt.VERTICAL 284 | val cellBreadth = getDecoratedSide(cellView, perpendicularTilt) 285 | val offset = getLimited(perpendicularTilt) * -1 286 | val filled = offset + getDecoratedSide(cellView, tilt) 287 | 288 | fillNewSide(screenBreadth, filled, tilt, currentColumn, currentRow, recycler, screenLimit, direction) 289 | 290 | return cellBreadth 291 | } 292 | 293 | 294 | private fun fillNewSide(screenBreadth : Int, initialFilled : Int, tilt: Tilt, 295 | initialColumn : Int, initialRow : Int, 296 | recycler : RecyclerView.Recycler?, 297 | screenLimit : Int, direction: Direction) { 298 | 299 | var cellView : View? 300 | var filled = initialFilled 301 | var currentColumn = initialColumn 302 | var currentRow = initialRow 303 | 304 | // do subsequent 305 | while (screenBreadth - filled >= 0) { 306 | 307 | cellView = if (tilt == Tilt.VERTICAL) 308 | layoutChildView(recycler, currentColumn, currentRow, filled, screenLimit, direction, tilt) 309 | else layoutChildView(recycler, currentColumn, currentRow, screenLimit, filled, direction, tilt) 310 | 311 | filled = filled + getDecoratedSide(cellView, tilt) 312 | if (tilt == Tilt.VERTICAL) currentColumn++ 313 | else currentRow++ 314 | } 315 | 316 | } 317 | 318 | 319 | private fun getDecoratedSide(child : View?, tilt: Tilt) = 320 | if (tilt == Tilt.HORIZONTAL) getDecoratedMeasuredNullableHeight(child) 321 | else getDecoratedMeasuredNullableWidth(child) 322 | 323 | 324 | private fun getLimited(tilt : Tilt) : Int { 325 | val smallestSoFar = getSmallest(tilt) 326 | 327 | val topLeft = if (tilt == Tilt.VERTICAL) getChildAt(0)?.bottom ?: 0 328 | else getChildAt(0)?.right ?: 0 329 | 330 | val sm = topLeft + (smallestSoFar * -1) 331 | return sm 332 | } 333 | 334 | 335 | private fun getSmallest(tilt : Tilt) : Int { 336 | var smallestSoFar = 2140000000 337 | 338 | for (i in 0 until childCount) { 339 | val child = getChildAt(i) ?: continue 340 | if (cellIsMarker(child)) continue 341 | 342 | val current = if (tilt == Tilt.VERTICAL) child.top 343 | else child.left 344 | 345 | if (current < smallestSoFar) { 346 | smallestSoFar = current 347 | } 348 | } 349 | 350 | return smallestSoFar 351 | } 352 | 353 | 354 | private fun getFarthest(tilt: Tilt): Int { 355 | var biggestSoFar = -1 356 | 357 | for (i in 0 until childCount) { 358 | val child = getChildAt(i) 359 | if (child == null) continue 360 | 361 | val current = if (tilt == Tilt.VERTICAL) child.bottom else child.right 362 | 363 | if (current > biggestSoFar) { 364 | biggestSoFar = current 365 | } 366 | } 367 | return biggestSoFar 368 | } 369 | 370 | 371 | private fun offsetCells(d : Int, tag: String) { 372 | for (i in 1 until childCount) { 373 | val child = getChildAt(i) 374 | 375 | if (child?.tag == CELL || child?.tag == tag) { 376 | when (tag) { 377 | COLUMN -> child.offsetTopAndBottom(-d) 378 | ROW -> child.offsetLeftAndRight(-d) 379 | } 380 | } 381 | } 382 | } 383 | 384 | 385 | // XXX: maybe scrap within scroll 386 | // maybe increase height/depth 387 | // maybe relayout screen when shifting directions 388 | // TODO: scrapping is probably premature, maybe don't scrap until whenever, and 389 | // send the information to getrows 390 | 391 | private fun scrapOffscreenViews(recycler: RecyclerView.Recycler?, direction : Direction, tilt: Tilt) { 392 | 393 | val initialRows = getSideCount(Side.ROWS) 394 | val initialColumns = getSideCount(Side.COLUMNS) // new 395 | 396 | if (recycler == null) return 397 | 398 | var bottomCellsGone = 0 399 | var rightCellsGone = 0 400 | 401 | // detaching in the other direction causes problems 402 | for (i in childCount-1 downTo 0) { 403 | 404 | val c = getChildAt(i) 405 | 406 | if (c == null) continue 407 | 408 | if (direction == Direction.LIMITED) { 409 | if ((c.top > height && tilt == Tilt.VERTICAL) || 410 | (c.left > width && tilt == Tilt.HORIZONTAL)) { 411 | dealWithView(recycler, c, tilt, direction) 412 | } 413 | } 414 | else { 415 | if (c.bottom < 0 && tilt == Tilt.VERTICAL) { 416 | bottomCellsGone++ 417 | dealWithView(recycler, c, tilt, direction) 418 | } 419 | 420 | if (c.right < 0 && tilt == Tilt.HORIZONTAL) { 421 | rightCellsGone++ 422 | dealWithView(recycler, c, tilt, direction) 423 | } 424 | } 425 | } 426 | 427 | sideAdjust(direction, initialRows, initialColumns, bottomCellsGone, rightCellsGone) 428 | } 429 | 430 | // TODO: considering recycling offscreen markers 431 | private fun dealWithView(recycler: RecyclerView.Recycler, c : View, tilt:Tilt, direction: Direction) { 432 | detachAndScrapView(c, recycler) 433 | } 434 | 435 | private fun sideAdjust(direction: Direction, initialRows: Int, initialColumns: Int, 436 | bottomCellsGone: Int, rightCellsGone: Int) { 437 | 438 | val rowDiff : Int 439 | val columnDiff : Int 440 | if (direction == Direction.UNLIMITED) { 441 | val rowsEnd = getSideCount(Side.ROWS) 442 | val columnsEnd = getSideCount(Side.COLUMNS) 443 | rowDiff = initialRows - rowsEnd 444 | columnDiff = initialColumns - columnsEnd 445 | } else if (initialColumns > 0 && initialRows > 0){ 446 | rowDiff = bottomCellsGone / initialColumns 447 | columnDiff = rightCellsGone / initialRows 448 | } else { 449 | rowDiff = 0 450 | columnDiff = 0 451 | } 452 | topRow = topRow + rowDiff 453 | leftColumn = leftColumn + columnDiff 454 | 455 | } 456 | 457 | 458 | private fun cellIsMarker(child: View) : Boolean { 459 | return child.z > 1.0f 460 | } 461 | 462 | private fun getSideCount(side: Side): Int { 463 | // we add it to a set as it needs uniqueness 464 | val cells = mutableSetOf() 465 | for (i in 0 until childCount) { 466 | val child = getChildAt(i) ?: continue 467 | if (cellIsMarker(child)) continue 468 | 469 | val mark = if (side == Side.ROWS) child.top 470 | else child.left 471 | 472 | cells.add(mark) 473 | } 474 | return cells.size 475 | } 476 | 477 | private class LayoutParams(width: Int, height: Int) : 478 | RecyclerView.LayoutParams(width, height) 479 | 480 | } 481 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/SheetViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import android.content.ContentResolver 4 | import android.net.Uri 5 | import android.webkit.MimeTypeMap 6 | import androidx.lifecycle.LiveData 7 | import androidx.lifecycle.MutableLiveData 8 | import androidx.lifecycle.Transformations 9 | import androidx.lifecycle.ViewModel 10 | import com.unwrappedapps.android.spreadsheet.PullState 11 | import com.unwrappedapps.android.spreadsheet.spreadsheet.Spreadsheet 12 | import java.io.File 13 | import java.io.IOException 14 | 15 | class SheetViewModel : ViewModel() { 16 | 17 | val spreadsheet = MutableLiveData() 18 | val uri = MutableLiveData() 19 | val contentResolver = MutableLiveData() 20 | var jumpCell = MutableLiveData() 21 | 22 | var leftColumn : Int = 1 23 | var topRow : Int = 1 24 | 25 | val sheetLoad = MutableLiveData() 26 | val sheetLoadState: LiveData = Transformations.switchMap(sheetLoad) { 27 | readTextFromUri(uri.value, contentResolver.value) 28 | } 29 | 30 | fun setTheJumpCell(cell : String) { 31 | setJumping(cell) 32 | jumpCell.value = cell 33 | } 34 | 35 | fun processUri(pUri: Uri, pContentResolver: ContentResolver) { 36 | uri.value = pUri 37 | contentResolver.value = pContentResolver 38 | sheetLoad.value = PullState.LOADING 39 | } 40 | 41 | @Throws(IOException::class) 42 | fun readTextFromUri(uri: Uri?, contentResolver: ContentResolver?) : LiveData { 43 | val pullState = MutableLiveData() 44 | pullState.value = PullState.LOADING 45 | 46 | val mimeType = getMimeType(contentResolver, uri) 47 | 48 | 49 | if (uri != null) { 50 | contentResolver?.openInputStream(uri)?.use { inputStream -> 51 | spreadsheet.value = Spreadsheet(inputStream, mimeType) 52 | pullState.value = PullState.LOADED_ACK 53 | } 54 | } 55 | 56 | return pullState 57 | } 58 | 59 | 60 | fun getMimeType(cr: ContentResolver?, uri: Uri?): String? { 61 | if (uri?.scheme == ContentResolver.SCHEME_CONTENT) { 62 | return MimeTypeMap.getSingleton().getExtensionFromMimeType(cr?.getType(uri)) 63 | } else { 64 | return MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(File(uri?.path)).toString()) 65 | } 66 | } 67 | 68 | fun setJumping(jumpCell: String) { 69 | var letter: String 70 | val number: String 71 | 72 | if (jumpCell.length > 0 && jumpCell.matches("^[A-Za-z]+[0-9]+".toRegex())) { 73 | number = jumpCell.replace("[A-Za-z]".toRegex(), "") 74 | letter = jumpCell.replace("[0-9]".toRegex(), "") 75 | val letterNum = doLetter(letter) 76 | val numberNum = number.toInt() 77 | topRow = numberNum 78 | leftColumn = letterNum 79 | } else { 80 | topRow = 1 81 | leftColumn = 1 82 | } 83 | } 84 | 85 | 86 | private fun doLetter(letter: String): Int { 87 | 88 | val zero = letter[0].toString() 89 | val numZero = letterToNumber(zero) 90 | 91 | if (letter.length > 1) { 92 | val one = letter[1].toString() 93 | val numOne = letterToNumber(one) 94 | if (letter.length > 2) { 95 | val two = letter[2].toString() 96 | val numTwo = letterToNumber(two) 97 | return numZero * 26 * 26 + numOne * 26 + numTwo 98 | } 99 | return numZero * 26 + numOne 100 | } else { 101 | return numZero 102 | } 103 | } 104 | 105 | 106 | private fun letterToNumber(letter: String): Int { 107 | 108 | val n: Int 109 | 110 | when (letter) { 111 | "A" -> n = 1 112 | "B" -> n = 2 113 | "C" -> n = 3 114 | "D" -> n = 4 115 | "E" -> n = 5 116 | "F" -> n = 6 117 | "G" -> n = 7 118 | "H" -> n = 8 119 | "I" -> n = 9 120 | "J" -> n = 10 121 | "K" -> n = 11 122 | "L" -> n = 12 123 | "M" -> n = 13 124 | "N" -> n = 14 125 | "O" -> n = 15 126 | "P" -> n = 16 127 | "Q" -> n = 17 128 | "R" -> n = 18 129 | "S" -> n = 19 130 | "T" -> n = 20 131 | "U" -> n = 21 132 | "V" -> n = 22 133 | "W" -> n = 23 134 | "X" -> n = 24 135 | "Y" -> n = 25 136 | "Z" -> n = 26 137 | else -> n = 1 138 | } 139 | return n 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /app/src/main/java/com/unwrappedapps/android/spreadsheet/ui/sheet/SheetViewPager.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet.ui.sheet 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.MotionEvent 6 | import androidx.viewpager.widget.ViewPager 7 | 8 | class SheetViewPager : ViewPager { 9 | 10 | constructor(context: Context) : super(context) { 11 | 12 | } 13 | 14 | constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) { 15 | 16 | } 17 | 18 | // no swiping 19 | override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { 20 | return false 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_find_in_page_black_18dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/drawable-xxxhdpi/ic_find_in_page_black_18dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_find_in_page_black_36dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/drawable-xxxhdpi/ic_find_in_page_black_36dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/file16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/drawable/file16.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/layout/graph_cell.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/jump_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/sheet_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/layout/tab_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_spreadsheet.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 11 | 12 | 16 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26a6ff 4 | #2686ff 5 | #D81B60 6 | #E3E3E3 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 12dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26A6FF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Spreadsheet 3 | Element 4 | Find 5 | Load file 6 | Jump to cell 7 | Enter cell (e.g. A1) 8 | Go 9 | Cancel 10 | Jump to cell: 11 | Sheet 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/test/java/com/unwrappedapps/android/spreadsheet/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/test/java/com/unwrappedapps/android/spreadsheet/PullTest.kt: -------------------------------------------------------------------------------- 1 | package com.unwrappedapps.android.spreadsheet 2 | 3 | import org.junit.Test 4 | import org.junit.Assert.* 5 | 6 | class PullTest { 7 | @Test 8 | fun pullstates_check() { 9 | val pullStateLoading = PullState.LOADING 10 | val pullStateLoadedAck = PullState.LOADED_ACK 11 | assertEquals(pullStateLoading, PullState.LOADING) 12 | assertNotEquals(pullStateLoading, pullStateLoadedAck) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext.kotlin_version = '1.3.21' 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.4.0' 11 | 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | android.useAndroidX=true 17 | android.enableJetifier=true 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dennis-sheil/android-spreadsheet/7ad59ec5902dbdef500e2ae5cfe42adcabb639b6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 20 17:33:52 EDT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------