├── SendReduced ├── build.gradle ├── lint.xml └── src │ ├── free │ └── res │ │ ├── drawable-hdpi │ │ └── icon.png │ │ ├── drawable-ldpi │ │ └── icon.png │ │ ├── drawable-mdpi │ │ └── icon.png │ │ ├── drawable-xhdpi │ │ └── icon.png │ │ ├── drawable-xxhdpi │ │ └── icon.png │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── paths.xml │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── licenses.txt │ ├── java │ │ └── mobi │ │ │ └── omegacentauri │ │ │ └── SendReduced │ │ │ ├── FileProvider.java │ │ │ ├── GetReduced.java │ │ │ ├── MarketDetector.java │ │ │ ├── Options.java │ │ │ ├── SendReduced.java │ │ │ ├── ShowLicense.java │ │ │ └── Utils.java │ └── res │ │ ├── values │ │ └── strings.xml │ │ └── xml │ │ └── options.xml │ └── pro │ └── res │ ├── drawable-hdpi │ └── icon.png │ ├── drawable-ldpi │ └── icon.png │ ├── drawable-mdpi │ └── icon.png │ ├── drawable-xhdpi │ └── icon.png │ ├── drawable-xxhdpi │ └── icon.png │ ├── values │ └── strings.xml │ └── xml │ └── paths.xml ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── icon114free.png ├── icon135free.png ├── icon512free.png ├── images ├── Lens_and_wavefronts.xcf ├── Lens_and_wavefrontsfree.png ├── Lens_and_wavefrontsfree.svg ├── Lens_and_wavefrontsfree.xcf └── Lens_and_wavefrontspro.png ├── import-summary.txt ├── license.txt ├── local.properties ├── scale.sh └── settings.gradle /SendReduced/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | signingConfigs { 5 | release { 6 | } 7 | } 8 | compileSdkVersion 29 9 | buildToolsVersion "30.0.3" 10 | 11 | defaultConfig { 12 | applicationId "mobi.omegacentauri.SendReduced" 13 | signingConfig signingConfigs.release 14 | minSdkVersion 14 15 | targetSdkVersion 30 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 22 | signingConfig signingConfigs.release 23 | } 24 | } 25 | 26 | productFlavors { 27 | pro { 28 | applicationId 'mobi.omegacentauri.SendReduced_pro' 29 | dimension 'pro_or_free' 30 | resValue 'string', 'license_intent', applicationId + ".LICENSE" 31 | } 32 | free { 33 | dimension 'pro_or_free' 34 | applicationId 'mobi.omegacentauri.SendReduced' 35 | resValue 'string', 'license_intent', applicationId + ".LICENSE" 36 | } 37 | } 38 | flavorDimensions 'pro_or_free' 39 | 40 | lintOptions { 41 | checkReleaseBuilds false 42 | //If you want to continue even if errors found use following line 43 | abortOnError false 44 | } 45 | 46 | allprojects { 47 | afterEvaluate { project -> 48 | def propsFile = rootProject.file('../SendReduced-keys.properties') 49 | def configName = 'release' 50 | 51 | if (propsFile.exists() && android.signingConfigs.hasProperty(configName)) { 52 | def props = new Properties() 53 | props.load(new FileInputStream(propsFile)) 54 | android.signingConfigs[configName].storeFile = file(props['storeFile']) 55 | android.signingConfigs[configName].storePassword = props['storePassword'] 56 | android.signingConfigs[configName].keyAlias = props['keyAlias'] 57 | android.signingConfigs[configName].keyPassword = props['keyPassword'] 58 | } 59 | } 60 | } 61 | } 62 | dependencies { 63 | implementation 'androidx.exifinterface:exifinterface:1.3.2' 64 | // compile 'com.android.support.exifinterface:25.1.0' 65 | } -------------------------------------------------------------------------------- /SendReduced/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /SendReduced/src/free/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/free/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/free/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/free/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/free/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/free/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/free/res/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/free/res/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/free/res/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/free/res/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/free/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Send Reduced Lite 4 | 5 | -------------------------------------------------------------------------------- /SendReduced/src/free/res/xml/paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /SendReduced/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | 16 | 18 | 19 | 20 | 23 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 83 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /SendReduced/src/main/assets/licenses.txt: -------------------------------------------------------------------------------- 1 |

FileProvider.java


2 | FileProvider.java Copyright (C) 2013 The Android Open Source Project and/or Google, Inc.
3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright [yyyy] [name of copyright owner] 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/FileProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | s * Copyright (C) 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package mobi.omegacentauri.SendReduced; 17 | import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; 18 | import static org.xmlpull.v1.XmlPullParser.START_TAG; 19 | import android.content.ContentProvider; 20 | import android.content.ContentValues; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.pm.PackageManager; 24 | import android.content.pm.ProviderInfo; 25 | import android.content.res.XmlResourceParser; 26 | import android.database.Cursor; 27 | import android.database.MatrixCursor; 28 | import android.net.Uri; 29 | import android.os.Environment; 30 | import android.os.ParcelFileDescriptor; 31 | import android.provider.OpenableColumns; 32 | import android.text.TextUtils; 33 | import android.util.Log; 34 | import android.webkit.MimeTypeMap; 35 | import org.xmlpull.v1.XmlPullParserException; 36 | import java.io.File; 37 | import java.io.FileNotFoundException; 38 | import java.io.IOException; 39 | import java.util.HashMap; 40 | import java.util.Map; 41 | /** 42 | * FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing 43 | * of files associated with an app by creating a content:// {@link Uri} for a file 44 | * instead of a file:/// {@link Uri}. 45 | *

46 | * A content URI allows you to grant read and write access using 47 | * temporary access permissions. When you create an {@link Intent} containing 48 | * a content URI, in order to send the content URI 49 | * to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add 50 | * permissions. These permissions are available to the client app for as long as the stack for 51 | * a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a 52 | * {@link android.app.Service}, the permissions are available as long as the 53 | * {@link android.app.Service} is running. 54 | *

55 | * In comparison, to control access to a file:/// {@link Uri} you have to modify the 56 | * file system permissions of the underlying file. The permissions you provide become available to 57 | * any app, and remain in effect until you change them. This level of access is 58 | * fundamentally insecure. 59 | *

60 | * The increased level of file access security offered by a content URI 61 | * makes FileProvider a key part of Android's security infrastructure. 62 | *

63 | * This overview of FileProvider includes the following topics: 64 | *

65 | *
    66 | *
  1. Defining a FileProvider
  2. 67 | *
  3. Specifying Available Files
  4. 68 | *
  5. Retrieving the Content URI for a File
  6. 69 | *
  7. Granting Temporary Permissions to a URI
  8. 70 | *
  9. Serving a Content URI to Another App
  10. 71 | *
72 | *

Defining a FileProvider

73 | *

74 | * Since the default functionality of FileProvider includes content URI generation for files, you 75 | * don't need to define a subclass in code. Instead, you can include a FileProvider in your app 76 | * by specifying it entirely in XML. To specify the FileProvider component itself, add a 77 | * <provider> 78 | * element to your app manifest. Set the android:name attribute to 79 | * android.support.v4.content.FileProvider. Set the android:authorities 80 | * attribute to a URI authority based on a domain you control; for example, if you control the 81 | * domain mydomain.com you should use the authority 82 | * com.mydomain.fileprovider. Set the android:exported attribute to 83 | * false; the FileProvider does not need to be public. Set the 84 | * android:grantUriPermissions attribute to true, to allow you 86 | * to grant temporary access to files. For example: 87 | *

 88 |  *<manifest>
 89 |  *    ...
 90 |  *    <application>
 91 |  *        ...
 92 |  *        <provider
 93 |  *            android:name="android.support.v4.content.FileProvider"
 94 |  *            android:authorities="com.mydomain.fileprovider"
 95 |  *            android:exported="false"
 96 |  *            android:grantUriPermissions="true">
 97 |  *            ...
 98 |  *        </provider>
 99 |  *        ...
100 |  *    </application>
101 |  *</manifest>
102 | *

103 | * If you want to override any of the default behavior of FileProvider methods, extend 104 | * the FileProvider class and use the fully-qualified class name in the android:name 105 | * attribute of the <provider> element. 106 | *

Specifying Available Files

107 | * A FileProvider can only generate a content URI for files in directories that you specify 108 | * beforehand. To specify a directory, specify the its storage area and path in XML, using child 109 | * elements of the <paths> element. 110 | * For example, the following paths element tells FileProvider that you intend to 111 | * request content URIs for the images/ subdirectory of your private file area. 112 | *
113 |  *<paths xmlns:android="http://schemas.android.com/apk/res/android">
114 |  *    <files-path name="my_images" path="images/"/>
115 |  *    ...
116 |  *</paths>
117 |  *
118 | *

119 | * The <paths> element must contain one or more of the following child elements: 120 | *

121 | *
122 | *
123 | *
124 |  *<files-path name="name" path="path" />
125 |  *
126 | *
127 | *
128 | * Represents files in the files/ subdirectory of your app's internal storage 129 | * area. This subdirectory is the same as the value returned by {@link Context#getFilesDir() 130 | * Context.getFilesDir()}. 131 | *
132 | *
133 |  *<external-path name="name" path="path" />
134 |  *
135 | *
136 | *
137 | * Represents files in the root of your app's external storage area. The path 138 | * {@link Context#getExternalFilesDir(String) Context.getExternalFilesDir()} returns the 139 | * files/ subdirectory of this this root. 140 | *
141 | *
142 | *
143 |  *<cache-path name="name" path="path" />
144 |  *
145 | *
146 | *
147 | * Represents files in the cache subdirectory of your app's internal storage area. The root path 148 | * of this subdirectory is the same as the value returned by {@link Context#getCacheDir() 149 | * getCacheDir()}. 150 | *
151 | *
152 | *

153 | * These child elements all use the same attributes: 154 | *

155 | *
156 | *
157 | * name="name" 158 | *
159 | *
160 | * A URI path segment. To enforce security, this value hides the name of the subdirectory 161 | * you're sharing. The subdirectory name for this value is contained in the 162 | * path attribute. 163 | *
164 | *
165 | * path="path" 166 | *
167 | *
168 | * The subdirectory you're sharing. While the name attribute is a URI path 169 | * segment, the path value is an actual subdirectory name. Notice that the 170 | * value refers to a subdirectory, not an individual file or files. You can't 171 | * share a single file by its file name, nor can you specify a subset of files using 172 | * wildcards. 173 | *
174 | *
175 | *

176 | * You must specify a child element of <paths> for each directory that contains 177 | * files for which you want content URIs. For example, these XML elements specify two directories: 178 | *

179 |  *<paths xmlns:android="http://schemas.android.com/apk/res/android">
180 |  *    <files-path name="my_images" path="images/"/>
181 |  *    <files-path name="my_docs" path="docs/"/>
182 |  *</paths>
183 |  *
184 | *

185 | * Put the <paths> element and its children in an XML file in your project. 186 | * For example, you can add them to a new file called res/xml/file_paths.xml. 187 | * To link this file to the FileProvider, add a 188 | * <meta-data> element 189 | * as a child of the <provider> element that defines the FileProvider. Set the 190 | * <meta-data> element's "android:name" attribute to 191 | * android.support.FILE_PROVIDER_PATHS. Set the element's "android:resource" attribute 192 | * to @xml/file_paths (notice that you don't specify the .xml 193 | * extension). For example: 194 | *

195 |  *<provider
196 |  *    android:name="android.support.v4.content.FileProvider"
197 |  *    android:authorities="com.mydomain.fileprovider"
198 |  *    android:exported="false"
199 |  *    android:grantUriPermissions="true">
200 |  *    <meta-data
201 |  *        android:name="android.support.FILE_PROVIDER_PATHS"
202 |  *        android:resource="@xml/file_paths" />
203 |  *</provider>
204 |  *
205 | *

Generating the Content URI for a File

206 | *

207 | * To share a file with another app using a content URI, your app has to generate the content URI. 208 | * To generate the content URI, create a new {@link File} for the file, then pass the {@link File} 209 | * to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI 210 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an 211 | * {@link android.content.Intent}. The client app that receives the content URI can open the file 212 | * and access its contents by calling 213 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) 214 | * ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}. 215 | *

216 | * For example, suppose your app is offering files to other apps with a FileProvider that has the 217 | * authority com.mydomain.fileprovider. To get a content URI for the file 218 | * default_image.jpg in the images/ subdirectory of your internal storage 219 | * add the following code: 220 | *

221 |  *File imagePath = new File(Context.getFilesDir(), "images");
222 |  *File newFile = new File(imagePath, "default_image.jpg");
223 |  *Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
224 |  *
225 | * As a result of the previous snippet, 226 | * {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI 227 | * content://com.mydomain.fileprovider/my_images/default_image.jpg. 228 | *

Granting Temporary Permissions to a URI

229 | * To grant an access permission to a content URI returned from 230 | * {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following: 231 | * 264 | *

Serving a Content URI to Another App

265 | *

266 | * There are a variety of ways to serve the content URI for a file to a client app. One common way 267 | * is for the client app to start your app by calling 268 | * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()}, 269 | * which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app. 270 | * In response, your app can immediately return a content URI to the client app or present a user 271 | * interface that allows the user to pick a file. In the latter case, once the user picks the file 272 | * your app can return its content URI. In both cases, your app returns the content URI in an 273 | * {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}. 274 | *

275 | *

276 | * You can also put the content URI in a {@link android.content.ClipData} object and then add the 277 | * object to an {@link Intent} you send to a client app. To do this, call 278 | * {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can 279 | * add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own 280 | * content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent} 281 | * to set temporary access permissions, the same permissions are applied to all of the content 282 | * URIs. 283 | *

284 | *

285 | * Note: The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is 286 | * only available in platform version 16 (Android 4.1) and later. If you want to maintain 287 | * compatibility with previous versions, you should send one content URI at a time in the 288 | * {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling 289 | * {@link Intent#setData setData()}. 290 | *

291 | *

More Information

292 | *

293 | * To learn more about FileProvider, see the Android training class 294 | * Sharing Files Securely with URIs. 295 | *

296 | */ 297 | public class FileProvider extends ContentProvider { 298 | private static final String[] COLUMNS = { 299 | OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; 300 | private static final String 301 | META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; 302 | private static final String TAG_ROOT_PATH = "root-path"; 303 | private static final String TAG_FILES_PATH = "files-path"; 304 | private static final String TAG_CACHE_PATH = "cache-path"; 305 | private static final String TAG_EXTERNAL = "external-path"; 306 | private static final String ATTR_NAME = "name"; 307 | private static final String ATTR_PATH = "path"; 308 | private static final File DEVICE_ROOT = new File("/"); 309 | // @GuardedBy("sCache") 310 | private static HashMap sCache = new HashMap(); 311 | private PathStrategy mStrategy; 312 | /** 313 | * The default FileProvider implementation does not need to be initialized. If you want to 314 | * override this method, you must provide your own subclass of FileProvider. 315 | */ 316 | @Override 317 | public boolean onCreate() { 318 | return true; 319 | } 320 | /** 321 | * After the FileProvider is instantiated, this method is called to provide the system with 322 | * information about the provider. 323 | * 324 | * @param context A {@link Context} for the current component. 325 | * @param info A {@link ProviderInfo} for the new provider. 326 | */ 327 | @Override 328 | public void attachInfo(Context context, ProviderInfo info) { 329 | super.attachInfo(context, info); 330 | // Sanity check our security 331 | if (info.exported) { 332 | throw new SecurityException("Provider must not be exported"); 333 | } 334 | if (!info.grantUriPermissions) { 335 | throw new SecurityException("Provider must grant uri permissions (oops)"); 336 | } 337 | mStrategy = getPathStrategy(context, info.authority); 338 | } 339 | /** 340 | * Return a content URI for a given {@link File}. Specific temporary 341 | * permissions for the content URI can be set with 342 | * {@link Context#grantUriPermission(String, Uri, int)}, or added 343 | * to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then 344 | * {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are 345 | * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and 346 | * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a 347 | * content {@link Uri} for file paths defined in their <paths> 348 | * meta-data element. See the Class Overview for more information. 349 | * 350 | * @param context A {@link Context} for the current component. 351 | * @param authority The authority of a {@link FileProvider} defined in a 352 | * {@code <provider>} element in your app's manifest. 353 | * @param file A {@link File} pointing to the filename for which you want a 354 | * content {@link Uri}. 355 | * @return A content URI for the file. 356 | * @throws IllegalArgumentException When the given {@link File} is outside 357 | * the paths supported by the provider. 358 | */ 359 | public static Uri getUriForFile(Context context, String authority, File file) { 360 | final PathStrategy strategy = getPathStrategy(context, authority); 361 | return strategy.getUriForFile(file); 362 | } 363 | /** 364 | * Use a content URI returned by 365 | * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file 366 | * managed by the FileProvider. 367 | * FileProvider reports the column names defined in {@link android.provider.OpenableColumns}: 368 | * 372 | * For more information, see 373 | * {@link ContentProvider#query(Uri, String[], String, String[], String) 374 | * ContentProvider.query()}. 375 | * 376 | * @param uri A content URI returned by {@link #getUriForFile}. 377 | * @param projection The list of columns to put into the {@link Cursor}. If null all columns are 378 | * included. 379 | * @param selection Selection criteria to apply. If null then all data that matches the content 380 | * URI is returned. 381 | * @param selectionArgs An array of {@link java.lang.String}, containing arguments to bind to 382 | * the selection parameter. The query method scans selection from left to 383 | * right and iterates through selectionArgs, replacing the current "?" character in 384 | * selection with the value at the current position in selectionArgs. The 385 | * values are bound to selection as {@link java.lang.String} values. 386 | * @param sortOrder A {@link java.lang.String} containing the column name(s) on which to sort 387 | * the resulting {@link Cursor}. 388 | * @return A {@link Cursor} containing the results of the query. 389 | * 390 | */ 391 | @Override 392 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 393 | String sortOrder) { 394 | // ContentProvider has already checked granted permissions 395 | final File file = mStrategy.getFileForUri(uri); 396 | if (projection == null) { 397 | projection = COLUMNS; 398 | } 399 | String[] cols = new String[projection.length]; 400 | Object[] values = new Object[projection.length]; 401 | int i = 0; 402 | for (String col : projection) { 403 | if (OpenableColumns.DISPLAY_NAME.equals(col)) { 404 | cols[i] = OpenableColumns.DISPLAY_NAME; 405 | values[i++] = file.getName(); 406 | } else if (OpenableColumns.SIZE.equals(col)) { 407 | cols[i] = OpenableColumns.SIZE; 408 | values[i++] = file.length(); 409 | } 410 | } 411 | cols = copyOf(cols, i); 412 | values = copyOf(values, i); 413 | final MatrixCursor cursor = new MatrixCursor(cols, 1); 414 | cursor.addRow(values); 415 | return cursor; 416 | } 417 | /** 418 | * Returns the MIME type of a content URI returned by 419 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 420 | * 421 | * @param uri A content URI returned by 422 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 423 | * @return If the associated file has an extension, the MIME type associated with that 424 | * extension; otherwise application/octet-stream. 425 | */ 426 | @Override 427 | public String getType(Uri uri) { 428 | // ContentProvider has already checked granted permissions 429 | final File file = mStrategy.getFileForUri(uri); 430 | final int lastDot = file.getName().lastIndexOf('.'); 431 | if (lastDot >= 0) { 432 | final String extension = file.getName().substring(lastDot + 1); 433 | final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 434 | if (mime != null) { 435 | return mime; 436 | } 437 | } 438 | return "application/octet-stream"; 439 | } 440 | /** 441 | * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must 442 | * subclass FileProvider if you want to provide different functionality. 443 | */ 444 | @Override 445 | public Uri insert(Uri uri, ContentValues values) { 446 | throw new UnsupportedOperationException("No external inserts"); 447 | } 448 | /** 449 | * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must 450 | * subclass FileProvider if you want to provide different functionality. 451 | */ 452 | @Override 453 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 454 | throw new UnsupportedOperationException("No external updates"); 455 | } 456 | /** 457 | * Deletes the file associated with the specified content URI, as 458 | * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this 459 | * method does not throw an {@link java.io.IOException}; you must check its return value. 460 | * 461 | * @param uri A content URI for a file, as returned by 462 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 463 | * @param selection Ignored. Set to {@code null}. 464 | * @param selectionArgs Ignored. Set to {@code null}. 465 | * @return 1 if the delete succeeds; otherwise, 0. 466 | */ 467 | @Override 468 | public int delete(Uri uri, String selection, String[] selectionArgs) { 469 | // ContentProvider has already checked granted permissions 470 | final File file = mStrategy.getFileForUri(uri); 471 | return file.delete() ? 1 : 0; 472 | } 473 | /** 474 | * By default, FileProvider automatically returns the 475 | * {@link ParcelFileDescriptor} for a file associated with a content:// 476 | * {@link Uri}. To get the {@link ParcelFileDescriptor}, call 477 | * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) 478 | * ContentResolver.openFileDescriptor}. 479 | * 480 | * To override this method, you must provide your own subclass of FileProvider. 481 | * 482 | * @param uri A content URI associated with a file, as returned by 483 | * {@link #getUriForFile(Context, String, File) getUriForFile()}. 484 | * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and 485 | * write access, or "rwt" for read and write access that truncates any existing file. 486 | * @return A new {@link ParcelFileDescriptor} with which you can access the file. 487 | */ 488 | @Override 489 | public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 490 | // ContentProvider has already checked granted permissions 491 | SendReduced.log("openFile "+uri); 492 | final File file = mStrategy.getFileForUri(uri); 493 | final int fileMode = modeToMode(mode); 494 | return ParcelFileDescriptor.open(file, fileMode); 495 | } 496 | /** 497 | * Return {@link PathStrategy} for given authority, either by parsing or 498 | * returning from cache. 499 | */ 500 | private static PathStrategy getPathStrategy(Context context, String authority) { 501 | PathStrategy strat; 502 | synchronized (sCache) { 503 | strat = sCache.get(authority); 504 | if (strat == null) { 505 | try { 506 | strat = parsePathStrategy(context, authority); 507 | } catch (IOException e) { 508 | throw new IllegalArgumentException( 509 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); 510 | } catch (XmlPullParserException e) { 511 | throw new IllegalArgumentException( 512 | "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); 513 | } 514 | sCache.put(authority, strat); 515 | } 516 | } 517 | return strat; 518 | } 519 | /** 520 | * Parse and return {@link PathStrategy} for given authority as defined in 521 | * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}. 522 | * 523 | * @see #getPathStrategy(Context, String) 524 | */ 525 | private static PathStrategy parsePathStrategy(Context context, String authority) 526 | throws IOException, XmlPullParserException { 527 | final SimplePathStrategy strat = new SimplePathStrategy(authority); 528 | final ProviderInfo info = context.getPackageManager() 529 | .resolveContentProvider(authority, PackageManager.GET_META_DATA); 530 | final XmlResourceParser in = info.loadXmlMetaData( 531 | context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); 532 | if (in == null) { 533 | throw new IllegalArgumentException( 534 | "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); 535 | } 536 | int type; 537 | while ((type = in.next()) != END_DOCUMENT) { 538 | if (type == START_TAG) { 539 | final String tag = in.getName(); 540 | final String name = in.getAttributeValue(null, ATTR_NAME); 541 | String path = in.getAttributeValue(null, ATTR_PATH); 542 | File target = null; 543 | if (TAG_ROOT_PATH.equals(tag)) { 544 | target = buildPath(DEVICE_ROOT, path); 545 | } else if (TAG_FILES_PATH.equals(tag)) { 546 | target = buildPath(context.getFilesDir(), path); 547 | } else if (TAG_CACHE_PATH.equals(tag)) { 548 | target = buildPath(context.getCacheDir(), path); 549 | } else if (TAG_EXTERNAL.equals(tag)) { 550 | target = buildPath(Environment.getExternalStorageDirectory(), path); 551 | } 552 | if (target != null) { 553 | strat.addRoot(name, target); 554 | } 555 | } 556 | } 557 | return strat; 558 | } 559 | /** 560 | * Strategy for mapping between {@link File} and {@link Uri}. 561 | *

562 | * Strategies must be symmetric so that mapping a {@link File} to a 563 | * {@link Uri} and then back to a {@link File} points at the original 564 | * target. 565 | *

566 | * Strategies must remain consistent across app launches, and not rely on 567 | * dynamic state. This ensures that any generated {@link Uri} can still be 568 | * resolved if your process is killed and later restarted. 569 | * 570 | * @see SimplePathStrategy 571 | */ 572 | interface PathStrategy { 573 | /** 574 | * Return a {@link Uri} that represents the given {@link File}. 575 | */ 576 | public Uri getUriForFile(File file); 577 | /** 578 | * Return a {@link File} that represents the given {@link Uri}. 579 | */ 580 | public File getFileForUri(Uri uri); 581 | } 582 | /** 583 | * Strategy that provides access to files living under a narrow whitelist of 584 | * filesystem roots. It will throw {@link SecurityException} if callers try 585 | * accessing files outside the configured roots. 586 | *

587 | * For example, if configured with 588 | * {@code addRoot("myfiles", context.getFilesDir())}, then 589 | * {@code context.getFileStreamPath("foo.txt")} would map to 590 | * {@code content://myauthority/myfiles/foo.txt}. 591 | */ 592 | static class SimplePathStrategy implements PathStrategy { 593 | private final String mAuthority; 594 | private final HashMap mRoots = new HashMap(); 595 | public SimplePathStrategy(String authority) { 596 | mAuthority = authority; 597 | } 598 | /** 599 | * Add a mapping from a name to a filesystem root. The provider only offers 600 | * access to files that live under configured roots. 601 | */ 602 | public void addRoot(String name, File root) { 603 | if (TextUtils.isEmpty(name)) { 604 | throw new IllegalArgumentException("Name must not be empty"); 605 | } 606 | try { 607 | // Resolve to canonical path to keep path checking fast 608 | root = root.getCanonicalFile(); 609 | } catch (IOException e) { 610 | throw new IllegalArgumentException( 611 | "Failed to resolve canonical path for " + root, e); 612 | } 613 | mRoots.put(name, root); 614 | } 615 | @Override 616 | public Uri getUriForFile(File file) { 617 | String path; 618 | try { 619 | path = file.getCanonicalPath(); 620 | } catch (IOException e) { 621 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file); 622 | } 623 | Log.v("SendReduced", "path "+path); 624 | // Find the most-specific root path 625 | Map.Entry mostSpecific = null; 626 | for (Map.Entry root : mRoots.entrySet()) { 627 | final String rootPath = root.getValue().getPath(); 628 | Log.v("SendReduced", "rootPath "+rootPath); 629 | if (path.startsWith(rootPath) && (mostSpecific == null 630 | || rootPath.length() > mostSpecific.getValue().getPath().length())) { 631 | mostSpecific = root; 632 | } 633 | } 634 | if (mostSpecific == null) { 635 | throw new IllegalArgumentException( 636 | "Failed to find configured root that contains " + path); 637 | } 638 | // Start at first char of path under root 639 | final String rootPath = mostSpecific.getValue().getPath(); 640 | if (rootPath.endsWith("/")) { 641 | path = path.substring(rootPath.length()); 642 | } else { 643 | path = path.substring(rootPath.length() + 1); 644 | } 645 | // Encode the tag and path separately 646 | path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); 647 | return new Uri.Builder().scheme("content") 648 | .authority(mAuthority).encodedPath(path).build(); 649 | } 650 | @Override 651 | public File getFileForUri(Uri uri) { 652 | String path = uri.getEncodedPath(); 653 | final int splitIndex = path.indexOf('/', 1); 654 | final String tag = Uri.decode(path.substring(1, splitIndex)); 655 | path = Uri.decode(path.substring(splitIndex + 1)); 656 | final File root = mRoots.get(tag); 657 | if (root == null) { 658 | throw new IllegalArgumentException("Unable to find configured root for " + uri); 659 | } 660 | File file = new File(root, path); 661 | try { 662 | file = file.getCanonicalFile(); 663 | } catch (IOException e) { 664 | throw new IllegalArgumentException("Failed to resolve canonical path for " + file); 665 | } 666 | if (!file.getPath().startsWith(root.getPath())) { 667 | throw new SecurityException("Resolved path jumped beyond configured root"); 668 | } 669 | return file; 670 | } 671 | } 672 | /** 673 | * Copied from ContentResolver.java 674 | */ 675 | private static int modeToMode(String mode) { 676 | int modeBits; 677 | if ("r".equals(mode)) { 678 | modeBits = ParcelFileDescriptor.MODE_READ_ONLY; 679 | } else if ("w".equals(mode) || "wt".equals(mode)) { 680 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY 681 | | ParcelFileDescriptor.MODE_CREATE 682 | | ParcelFileDescriptor.MODE_TRUNCATE; 683 | } else if ("wa".equals(mode)) { 684 | modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY 685 | | ParcelFileDescriptor.MODE_CREATE 686 | | ParcelFileDescriptor.MODE_APPEND; 687 | } else if ("rw".equals(mode)) { 688 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE 689 | | ParcelFileDescriptor.MODE_CREATE; 690 | } else if ("rwt".equals(mode)) { 691 | modeBits = ParcelFileDescriptor.MODE_READ_WRITE 692 | | ParcelFileDescriptor.MODE_CREATE 693 | | ParcelFileDescriptor.MODE_TRUNCATE; 694 | } else { 695 | throw new IllegalArgumentException("Invalid mode: " + mode); 696 | } 697 | return modeBits; 698 | } 699 | private static File buildPath(File base, String... segments) { 700 | File cur = base; 701 | for (String segment : segments) { 702 | if (segment != null) { 703 | cur = new File(cur, segment); 704 | } 705 | } 706 | return cur; 707 | } 708 | private static String[] copyOf(String[] original, int newLength) { 709 | final String[] result = new String[newLength]; 710 | System.arraycopy(original, 0, result, 0, newLength); 711 | return result; 712 | } 713 | private static Object[] copyOf(Object[] original, int newLength) { 714 | final Object[] result = new Object[newLength]; 715 | System.arraycopy(original, 0, result, 0, newLength); 716 | return result; 717 | } 718 | } -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/GetReduced.java: -------------------------------------------------------------------------------- 1 | package mobi.omegacentauri.SendReduced; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.pm.PackageManager; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.widget.Toast; 9 | 10 | public class GetReduced extends Activity { 11 | private static final int REQUEST_LOAD_IMAGE = 1; 12 | 13 | /** Called when the activity is first created. */ 14 | @Override 15 | public void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | 18 | Intent i = getIntent(); 19 | Bundle e = i.getExtras(); 20 | 21 | if (e != null && e.containsKey(Utils.INTENT_FROM_ME)) { 22 | PackageManager pm = getPackageManager(); 23 | pm.clearPackagePreferredActivities(getPackageName()); 24 | Toast.makeText(this, "Oops: You just used SendReduced to get your photo from SendReduced. You need to set a different source from SendReduced to avoid an endless loop.", Toast.LENGTH_LONG).show(); 25 | finish(); 26 | return; 27 | } 28 | 29 | if (i.getAction().equals(Intent.ACTION_GET_CONTENT) || 30 | i.getAction().equals(Intent.ACTION_PICK)) { 31 | Intent pick = new Intent(Intent.ACTION_PICK, 32 | android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 33 | Utils.startActivityForResultWithChooser(this, pick, "Choose source for photo to reduce", REQUEST_LOAD_IMAGE); 34 | } 35 | } 36 | 37 | @Override 38 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 39 | SendReduced.log("Codes "+requestCode+" "+resultCode); 40 | if (requestCode != REQUEST_LOAD_IMAGE || 41 | resultCode != RESULT_OK || 42 | data == null) { 43 | setResult(RESULT_CANCELED); 44 | } 45 | else { 46 | Utils utils = new Utils(this); 47 | Uri uri = utils.offerReduced(data.getData()); 48 | if (uri == null) 49 | setResult(RESULT_CANCELED); 50 | else { 51 | Intent i = new Intent().setData(uri); 52 | SendReduced.log("Passing "+uri); 53 | i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 54 | setResult(RESULT_OK, i); 55 | } 56 | } 57 | finish(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/MarketDetector.java: -------------------------------------------------------------------------------- 1 | package mobi.omegacentauri.SendReduced; 2 | import android.content.Context; 3 | import android.content.Intent; 4 | import android.content.pm.PackageManager; 5 | import android.content.pm.PackageManager.NameNotFoundException; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | 9 | public class MarketDetector { 10 | public static final boolean NO_MARKET = false; 11 | public static final int MARKET = 0; 12 | public static final int APPSTORE = 1; 13 | 14 | public static void launch(Context c) { 15 | Intent i = new Intent(Intent.ACTION_VIEW); 16 | i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 17 | if (detect(c) == MARKET) 18 | i.setData(Uri.parse("market://search?q=pub:\"Omega Centauri Software\"")); 19 | else 20 | i.setData(Uri.parse("http://www.amazon.com/gp/mas/dl/android?p=mobi.omegacentauri.ScreenDim.Full&showAll=1")); 21 | c.startActivity(i); 22 | } 23 | 24 | public static int detect(Context c) { 25 | if (Build.VERSION.SDK_INT < 5) 26 | return APPSTORE; 27 | 28 | PackageManager pm = c.getPackageManager(); 29 | 30 | String installer = pm.getInstallerPackageName(c.getPackageName()); 31 | 32 | if (installer != null && installer.equals("com.android.vending")) 33 | return MARKET; 34 | 35 | if (Build.MODEL.equalsIgnoreCase("Kindle Fire")) 36 | return APPSTORE; 37 | 38 | try { 39 | if (pm.getPackageInfo("com.amazon.venezia", 0) != null) 40 | return APPSTORE; 41 | } catch (NameNotFoundException e) { 42 | } 43 | 44 | return MARKET; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/Options.java: -------------------------------------------------------------------------------- 1 | package mobi.omegacentauri.SendReduced; 2 | 3 | import java.util.Arrays; 4 | 5 | import android.content.Intent; 6 | import android.content.SharedPreferences; 7 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener; 8 | import android.content.res.Resources; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.preference.Preference; 13 | import android.preference.PreferenceActivity; 14 | import android.preference.PreferenceManager; 15 | import android.preference.PreferenceScreen; 16 | import android.widget.Toast; 17 | 18 | public class Options extends PreferenceActivity implements OnSharedPreferenceChangeListener { 19 | public static final String PREF_RESOLUTION = "resolution"; 20 | public static final String PREF_QUALITY = "quality"; 21 | public static final String PREF_NAME = "output"; 22 | public static final String OPT_NAME_DATE_TIME = "date and time"; 23 | public static final String OPT_NAME_RANDOM = "random"; 24 | public static final String OPT_NAME_SEQUENTIAL = "sequential"; 25 | public static final String OPT_NAME_PRESERVE = "preserve"; 26 | public static final String PREF_EXIF_LOCATION = "exifLocation"; 27 | public static final String PREF_EXIF_MAKE_MODEL = "exifMake"; 28 | public static final String PREF_EXIF_DATETIME = "exifDateTime"; 29 | public static final String PREF_EXIF_SETTINGS = "exifSettings"; 30 | public static final String PREF_INCLUDE_DIRECT = "includeDirect"; 31 | public static final String PREF_SUPER_STRIP = "superStrip"; 32 | private static final String PREF_CONTENT_PROVIDER = "contentProvider2"; 33 | 34 | public static final String[] proKeys = { PREF_NAME, PREF_EXIF_LOCATION, PREF_EXIF_MAKE_MODEL, PREF_EXIF_DATETIME, PREF_SUPER_STRIP, "outputPrivacy" }; 35 | 36 | private static String[] summaryKeys = { PREF_RESOLUTION, PREF_QUALITY, PREF_NAME }; 37 | private static int[] summaryEntryValues = { R.array.resolutions, R.array.qualities, R.array.outputs }; 38 | private static int[] summaryEntries = { R.array.resolutions, R.array.qualities, R.array.outputs }; 39 | private static String[] summaryDefaults = { "1024", "85", "random" }; 40 | 41 | @Override 42 | public void onCreate(Bundle icicle) { 43 | super.onCreate(icicle); 44 | 45 | addPreferencesFromResource(R.xml.options); 46 | Utils.cleanCache(this, System.currentTimeMillis()); 47 | } 48 | @Override 49 | public void onResume() { 50 | super.onResume(); 51 | 52 | getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); 53 | customizeDisplay(); 54 | } 55 | 56 | @Override 57 | public void onStop() { 58 | super.onStop(); 59 | } 60 | 61 | 62 | @Override 63 | protected void onPause() { 64 | super.onPause(); 65 | getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); 66 | } 67 | 68 | static private boolean defaultContentProviderSetting() { 69 | return Build.VERSION.SDK_INT >= 23; 70 | } 71 | 72 | static public boolean useContentProvider(SharedPreferences options) { 73 | return Build.VERSION.SDK_INT >= 23; 74 | /* if (Build.VERSION.SDK_INT >= 28) 75 | return true; 76 | else if (Build.VERSION.SDK_INT < 23) 77 | return false; 78 | else 79 | return options.getBoolean(PREF_CONTENT_PROVIDER, defaultContentProviderSetting()); */ 80 | } 81 | 82 | @Override 83 | public void onSharedPreferenceChanged(SharedPreferences options, String key) { 84 | if (! SendReduced.pro(this) && Arrays.asList(proKeys).contains(key)) { 85 | Toast.makeText(this, "This setting only works in the Pro version", Toast.LENGTH_LONG).show(); 86 | } 87 | setSummary(key); 88 | } 89 | 90 | public static String getString(SharedPreferences options, String key) { 91 | for (int i=0; i= 28) { 104 | Preference pref = findPreference(PREF_CONTENT_PROVIDER); 105 | if (pref != null) 106 | getPreferenceScreen().removePreference(pref); 107 | } 108 | 109 | SharedPreferences options = PreferenceManager.getDefaultSharedPreferences(this); 110 | if (! options.contains(PREF_CONTENT_PROVIDER)) { 111 | SharedPreferences.Editor ed = options.edit(); 112 | ed.putBoolean(PREF_CONTENT_PROVIDER, defaultContentProviderSetting()); 113 | ed.commit(); 114 | } 115 | 116 | PreferenceScreen upgrade = (PreferenceScreen) findPreference("upgrade"); 117 | if (SendReduced.pro(this)) { 118 | if (upgrade != null) 119 | getPreferenceScreen().removePreference(upgrade); 120 | } 121 | else { 122 | Intent intent = new Intent(Intent.ACTION_VIEW); 123 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 124 | if (MarketDetector.detect(this) == MarketDetector.APPSTORE) { 125 | // string split up to fool switcher.sh 126 | intent.setData(Uri.parse("http://www.amazon.com/gp/mas/dl/android?p=mobi.omegacentauri.Send"+"Reduced_"+"pro")); 127 | } 128 | else { 129 | // string split up to fool switcher.sh 130 | intent.setData(Uri.parse("market://details?id=mobi.omegacentauri.Send" +"Reduced_"+"pro")); 131 | } 132 | 133 | if (upgrade != null) 134 | upgrade.setIntent(intent); 135 | 136 | for (String p : proKeys) { 137 | Preference pref = findPreference(p); 138 | if (pref != null) { 139 | String title = pref.getTitle().toString(); 140 | if (! title.endsWith("[pro]")) 141 | pref.setTitle(title + " [pro]"); 142 | } 143 | } 144 | 145 | PreferenceScreen ps = getPreferenceScreen(); 146 | int n = ps.getPreferenceCount(); 147 | for (int i = 0 ; i < n ; i++) { 148 | Preference p = ps.getPreference(i); 149 | String title = p.getTitle().toString(); 150 | if (title != null && title.contains(" [pro]")) { 151 | p.setTitle(title.replace(" [pro]", "")); 152 | } 153 | } 154 | } 155 | } 156 | 157 | public void setSummary(String key) { 158 | for (int i=0; i in = e.getParcelableArrayList(Intent.EXTRA_STREAM); 64 | ArrayList out = new ArrayList(); 65 | Utils utils = new Utils(this); 66 | 67 | for (Uri uri: in) { 68 | Uri reduced = utils.reduce(uri); 69 | if (reduced != null) 70 | out.add(reduced); 71 | } 72 | 73 | if (out.size()>0) { 74 | if (! options.getString(Options.PREF_NAME, Options.OPT_NAME_RANDOM).equals(Options.OPT_NAME_RANDOM)) 75 | Collections.sort(out); 76 | 77 | Intent go = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE); 78 | //go.setType("text/plain"); 79 | go.setType(Utils.MIME_TYPE); 80 | go.putParcelableArrayListExtra(android.content.Intent.EXTRA_STREAM, out); 81 | Uri[] grant = new Uri[out.size()]; 82 | out.toArray(grant); 83 | Utils.startWithChooser(this, grant, go); 84 | // startActivity(go); 85 | } 86 | } 87 | } 88 | else { 89 | finish(); 90 | } 91 | // String alpha; 92 | // alpha = null; 93 | // Log.v("",""+alpha.length()); 94 | 95 | } 96 | 97 | private void crashLogHandler() { 98 | if(!(Thread.getDefaultUncaughtExceptionHandler() instanceof MyCrashHandler)) 99 | Thread.setDefaultUncaughtExceptionHandler(new MyCrashHandler()); 100 | } 101 | 102 | class MyCrashHandler implements UncaughtExceptionHandler { 103 | 104 | @Override 105 | public void uncaughtException(Thread thread, Throwable ex) { 106 | Intent i = new Intent(Intent.ACTION_SEND); 107 | i.putExtra(Intent.EXTRA_SUBJECT, "crash report for "+getPackageName()); 108 | i.putExtra(Intent.EXTRA_EMAIL, new String[] {"arpruss@gmail.com"} ); 109 | StringWriter tw = new StringWriter(); 110 | ex.printStackTrace(new PrintWriter(tw)); 111 | i.putExtra(Intent.EXTRA_TEXT, ex.getMessage()+"\n"+tw.toString()); 112 | i.setType("message/rfc822"); 113 | startActivity(i); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/ShowLicense.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008-2009 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy of 6 | * the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations under 14 | * the License. 15 | */ 16 | 17 | package mobi.omegacentauri.SendReduced; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.InputStreamReader; 23 | 24 | import android.app.AlertDialog; 25 | import android.content.Context; 26 | import android.content.DialogInterface; 27 | import android.os.Bundle; 28 | import android.preference.PreferenceActivity; 29 | import android.text.Html; 30 | 31 | public class ShowLicense extends PreferenceActivity { 32 | static private String getStreamFile(InputStream stream) { 33 | BufferedReader reader; 34 | try { 35 | reader = new BufferedReader(new InputStreamReader(stream)); 36 | 37 | String text = ""; 38 | String line; 39 | while (null != (line=reader.readLine())) 40 | text = text + line; 41 | return text; 42 | } catch (IOException e) { 43 | // TODO Auto-generated catch block 44 | return ""; 45 | } 46 | } 47 | 48 | static public String getAssetFile(Context context, String assetName) { 49 | try { 50 | return getStreamFile(context.getAssets().open(assetName)); 51 | } catch (IOException e) { 52 | // TODO Auto-generated catch block 53 | return ""; 54 | } 55 | } 56 | 57 | @Override 58 | protected void onCreate(Bundle icicle) { 59 | super.onCreate(icicle); 60 | AlertDialog alertDialog = new AlertDialog.Builder(this).create(); 61 | alertDialog.setTitle("Licenses and copyrights"); 62 | alertDialog.setMessage(Html.fromHtml(getAssetFile(this, "licenses.txt"))); 63 | alertDialog.setButton(DialogInterface.BUTTON_POSITIVE, "OK", 64 | new DialogInterface.OnClickListener() { 65 | public void onClick(DialogInterface dialog, int which) {finish();} }); 66 | alertDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { 67 | public void onCancel(DialogInterface dialog) {finish();} }); 68 | alertDialog.show(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /SendReduced/src/main/java/mobi/omegacentauri/SendReduced/Utils.java: -------------------------------------------------------------------------------- 1 | package mobi.omegacentauri.SendReduced; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.io.FileFilter; 6 | import java.io.FileNotFoundException; 7 | import java.io.FileOutputStream; 8 | import java.io.FilenameFilter; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.text.ParseException; 13 | import java.text.SimpleDateFormat; 14 | import java.util.ArrayList; 15 | import java.util.Date; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | import android.annotation.SuppressLint; 21 | import android.app.Activity; 22 | import android.content.ComponentName; 23 | import android.content.ContentResolver; 24 | import android.content.Context; 25 | import android.content.Intent; 26 | import android.content.SharedPreferences; 27 | import android.content.pm.PackageManager; 28 | import android.content.pm.ResolveInfo; 29 | import android.database.Cursor; 30 | import android.graphics.Bitmap; 31 | import android.graphics.BitmapFactory; 32 | import android.graphics.Matrix; 33 | import androidx.exifinterface.media.ExifInterface; 34 | import android.net.Uri; 35 | import android.os.Build; 36 | import android.os.Environment; 37 | import android.preference.PreferenceManager; 38 | import android.provider.MediaStore; 39 | import android.provider.MediaStore.Images; 40 | import android.util.Log; 41 | 42 | public class Utils { 43 | static final int BUFSIZE = 16384; 44 | static final int INVALID_ROTATION = -360000; 45 | static final String PREFIX = "Image"; 46 | static final long CLEAN_TIME = (2 * 3600 * 1000l); 47 | Activity activity; 48 | ContentResolver cr; 49 | SharedPreferences options; 50 | private int outResolution; 51 | private int outQuality; 52 | private int sequencePos; 53 | static final String MIME_TYPE = "image/jpeg"; //"text/plain"; 54 | public static final String INTENT_FROM_ME = "mobi.omegacentauri.Send" + "Reduced.INTENT_FROM_ME"; 55 | private String curCacheDir; 56 | private long curTime; 57 | 58 | public Utils(Activity a) { 59 | activity = a; 60 | cr = a.getContentResolver(); 61 | options = PreferenceManager.getDefaultSharedPreferences(a); 62 | outResolution = Integer.parseInt(options.getString(Options.PREF_RESOLUTION, "1024")); 63 | outQuality = Integer.parseInt(options.getString(Options.PREF_QUALITY, "85")); 64 | sequencePos = 1; 65 | 66 | curTime = System.currentTimeMillis(); 67 | cleanCache(a, curTime); 68 | curCacheDir = null; 69 | String base = getCacheDir(a).getPath() + "/" + curTime; 70 | for (int i = 0 ; i < 1000000 ; i++) { 71 | String n = base + "-" + i; 72 | if (! new File(n).exists()) { 73 | curCacheDir = n; 74 | break; 75 | } 76 | } 77 | if (curCacheDir == null) 78 | curCacheDir = getCacheDir(a).getPath() + "/0"; 79 | new File(curCacheDir).mkdirs(); 80 | } 81 | 82 | public Uri reduce(Uri uri) { 83 | ReducedImage image = new ReducedImage(uri); 84 | if (image.bmp == null) 85 | return null; 86 | SendReduced.log("Reduced to "+image.bmp.getWidth()+"x"+image.bmp.getHeight()); 87 | String path = image.saveImage(); 88 | if (path == null) 89 | return null; 90 | if (fileProvider()) { 91 | return FileProvider.getUriForFile(activity, activity.getPackageName(), new File(path)); 92 | } 93 | else 94 | return Uri.fromFile(new File(path)); 95 | } 96 | 97 | public boolean sendReduced(Uri uri) { 98 | Uri out = reduce(uri); 99 | if (out == null) 100 | return false; 101 | Intent i = new Intent(android.content.Intent.ACTION_SEND); 102 | i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 103 | i.putExtra(android.content.Intent.EXTRA_STREAM, out); 104 | i.setType(MIME_TYPE); 105 | // i.putExtra(Intent.EXTRA_TEXT, " "); 106 | SendReduced.log("uri: "+out); 107 | startWithChooser(activity, new Uri[] { out }, i); 108 | return true; 109 | } 110 | 111 | static void startActivityForResultWithChooser(Activity activity, Intent i, String title, int code) { 112 | i.putExtra(Utils.INTENT_FROM_ME, true); 113 | activity.startActivityForResult(i, code); 114 | } 115 | 116 | static void startWithChooser(Activity activity, Uri[] grant, Intent i) { 117 | startWithChooser(activity, grant, i, "Share reduced photo with..."); 118 | } 119 | 120 | @SuppressLint("WrongConstant") 121 | private static void startWithChooser(Activity activity, Uri[] grant, Intent i, String title) { 122 | i.putExtra(INTENT_FROM_ME, true); 123 | i.setFlags(PackageManager.MATCH_DEFAULT_ONLY); //TODO:fix 124 | if (grant != null) { 125 | List possibles = activity.getPackageManager().queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); 126 | for (ResolveInfo r : possibles) { 127 | for (Uri uri : grant) 128 | activity.grantUriPermission(r.activityInfo.packageName, uri, 129 | Intent.FLAG_GRANT_READ_URI_PERMISSION); 130 | } 131 | } 132 | 133 | if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(Options.PREF_INCLUDE_DIRECT, true)) { 134 | i = Intent.createChooser(i, title); 135 | if (Build.VERSION.SDK_INT >= 24) { 136 | i.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, new ComponentName[]{activity.getComponentName()}); 137 | } 138 | } 139 | activity.startActivity(i); 140 | } 141 | 142 | public boolean fileProvider() { 143 | Boolean f = Options.useContentProvider(options); 144 | SendReduced.log("Using file provider? " + f); 145 | return f; 146 | } 147 | 148 | private static File getCacheDir(Context c) { 149 | File storage; 150 | 151 | if (Build.VERSION.SDK_INT >= 8) 152 | storage = c.getExternalCacheDir(); 153 | else 154 | storage = new File(Environment.getExternalStorageDirectory().getPath()+"/SendReduced"); 155 | 156 | storage.mkdirs(); 157 | return storage; 158 | } 159 | 160 | public static void cleanCache(Context c, Long curTime) { 161 | SendReduced.log("cleanCache "+getCacheDir(c)); 162 | File[] dirs = getCacheDir(c).listFiles(new FileFilter(){ 163 | 164 | @Override 165 | public boolean accept(File f) { 166 | return f.isDirectory(); 167 | }}); 168 | 169 | // previous version files 170 | for (File f : getCacheDir(c).listFiles()) { 171 | if (f.getPath().toLowerCase().endsWith(".jpg")) 172 | f.delete(); 173 | } 174 | 175 | for (File d : dirs) { 176 | String n = d.getName(); 177 | try { 178 | SendReduced.log("checking "+n); 179 | long t = Long.parseLong(n.split("-")[0]); 180 | SendReduced.log("parsed to "+t+" vs "+curTime); 181 | if (Math.abs(t-curTime) >= CLEAN_TIME) { 182 | SendReduced.log("cleaning up "+d.getPath()); 183 | for (File f : d.listFiles()) { 184 | f.delete(); 185 | try { 186 | c.revokeUriPermission(FileProvider.getUriForFile(c, c.getPackageName(), f), Intent.FLAG_GRANT_READ_URI_PERMISSION); 187 | } catch(Exception e) {} 188 | } 189 | d.delete(); 190 | } 191 | } 192 | catch(Exception e) { 193 | } 194 | } 195 | } 196 | 197 | static private int getOrientation(ContentResolver cr, Uri uri) { 198 | Cursor c = null; 199 | try { 200 | c = android.provider.MediaStore.Images.Media.query(cr, uri, 201 | new String[]{MediaStore.Images.Media.ORIENTATION}); 202 | } 203 | catch (Exception e) {} 204 | if (c == null) 205 | return INVALID_ROTATION; 206 | c.moveToFirst(); 207 | int columnIndex = c.getColumnIndex(MediaStore.Images.Media.ORIENTATION); 208 | if (columnIndex == -1) 209 | return INVALID_ROTATION; 210 | int o = c.getInt(columnIndex); 211 | c.close(); 212 | return o; 213 | } 214 | 215 | // static private String getPath(ContentResolver cr, Uri uri) { 216 | // Cursor c = 217 | // android.provider.MediaStore.Images.Media.query(cr, uri, 218 | // new String[]{MediaStore.Images.Media.DATA}); 219 | // if (c == null) 220 | // return null; 221 | // c.moveToFirst(); 222 | // String path = c.getString(c.getColumnIndex(MediaStore.Images.Media.DATA)); 223 | // c.close(); 224 | // return path; 225 | // } 226 | 227 | static private byte[] getStreamBytes(InputStream stream) { 228 | List chunks = new ArrayList(); 229 | byte[] buffer = new byte[BUFSIZE]; 230 | 231 | int total = 0; 232 | int read; 233 | try { 234 | while (0 <= (read = stream.read(buffer))) { 235 | byte[] chunk = new byte[read]; 236 | System.arraycopy(buffer, 0, chunk, 0, read); 237 | chunks.add(chunk); 238 | total += read; 239 | } 240 | } catch (IOException e) { 241 | SendReduced.log("error reading: "+e); 242 | return null; 243 | } 244 | 245 | byte[] data = new byte[total]; 246 | int pos = 0; 247 | 248 | for (byte[] chunk: chunks) { 249 | System.arraycopy(chunk, 0, data, pos, chunk.length); 250 | pos += chunk.length; 251 | } 252 | 253 | return data; 254 | } 255 | 256 | public Uri offerReduced(Uri uriIn) { 257 | ContentResolver cr = activity.getContentResolver(); 258 | // int orientation = getOrientation(cr, uriIn); 259 | // SendReduced.log("orientation: "+orientation); 260 | ReducedImage image = new ReducedImage(uriIn); 261 | if (image.bmp == null) 262 | return null; 263 | SendReduced.log("Reduced to "+image.bmp.getWidth()+"x"+image.bmp.getHeight()); 264 | String path = image.saveImage(); 265 | if (path == null) 266 | return null; 267 | else if (fileProvider()) 268 | return FileProvider.getUriForFile(activity, activity.getPackageName(), new File(path)); 269 | else 270 | return Uri.fromFile(new File(path)); 271 | } 272 | 273 | private static class SuperStripStream extends OutputStream { 274 | private enum jpegState { 275 | DEFAULT, 276 | COPYING, 277 | SKIPPING, 278 | ECS, 279 | ECS_MARKER, 280 | MARKER, 281 | DONE, 282 | COPYING_BEFORE_ECS; 283 | }; 284 | 285 | jpegState state = jpegState.DEFAULT; 286 | 287 | private final OutputStream rawStream; 288 | private int copyLength; 289 | private int skipLength; 290 | private int[] markerData = new int[18]; 291 | private int markerPosition; 292 | private boolean haveJFIF = false; 293 | 294 | public SuperStripStream(OutputStream os) { 295 | this.rawStream = os; 296 | this.state = jpegState.DEFAULT; 297 | } 298 | 299 | int markerDataGet16Bits(int position) { 300 | return ((0xFF & markerData[position]) << 8) | (0xFF & markerData[position+1]); 301 | } 302 | 303 | @Override 304 | public void write(int b) throws IOException { 305 | switch(this.state) { 306 | case DEFAULT: 307 | if ((b & 0xFF) == 0xFF) { 308 | this.state = jpegState.MARKER; 309 | markerData[0] = b; 310 | markerPosition = 1; 311 | } 312 | break; 313 | case COPYING: 314 | this.rawStream.write(b); 315 | copyLength--; 316 | if (copyLength <= 0) 317 | this.state = jpegState.DEFAULT; 318 | break; 319 | case COPYING_BEFORE_ECS: 320 | this.rawStream.write(b); 321 | copyLength--; 322 | if (copyLength <= 0) { 323 | this.state = jpegState.ECS; 324 | } 325 | break; 326 | case SKIPPING: 327 | skipLength--; 328 | if (skipLength <= 0) 329 | this.state = jpegState.DEFAULT; 330 | break; 331 | case ECS_MARKER: 332 | this.rawStream.write(b); 333 | if ((b & 0xFF) == 0xD9) { 334 | this.state = jpegState.DONE; 335 | } 336 | else { 337 | this.state = jpegState.ECS; 338 | } 339 | break; 340 | case ECS: 341 | this.rawStream.write(b); 342 | if ((b & 0xFF) == 0xFF) 343 | this.state = jpegState.ECS_MARKER; 344 | break; 345 | case MARKER: 346 | boolean writeMarker = false; 347 | markerData[markerPosition] = b; 348 | markerPosition++; 349 | if (markerPosition == 2) { 350 | if ((b & 0xFF) == 0xD8) { 351 | writeMarker = true; 352 | this.state = jpegState.DEFAULT; 353 | } 354 | } 355 | else if (markerPosition == 4) { 356 | int type = markerData[1] & 0xff; 357 | if (0xE1 <= type && type <= 0xEF) { 358 | skipLength = markerDataGet16Bits(2)-2; 359 | this.state = jpegState.SKIPPING; 360 | } 361 | else if (0xE0 != type) { 362 | writeMarker = true; 363 | copyLength = markerDataGet16Bits(2)-2; 364 | if (0xDA == type) { 365 | this.state = jpegState.COPYING_BEFORE_ECS; 366 | } 367 | else { 368 | this.state = jpegState.COPYING; 369 | } 370 | } 371 | else if (0xE0 == type) { 372 | // APP0 373 | if (haveJFIF) { 374 | // JFXX 375 | skipLength = markerDataGet16Bits(2)-2; 376 | this.state = jpegState.SKIPPING; 377 | } 378 | else { 379 | haveJFIF = true; 380 | } 381 | } 382 | } 383 | else if (markerPosition == 18) { 384 | // JFIF 385 | markerData[16] = 0; 386 | markerData[17] = 0; 387 | skipLength = markerDataGet16Bits(2)-16; 388 | markerData[2] = 0; 389 | markerData[3] = 18-2; 390 | writeMarker = true; 391 | state = jpegState.SKIPPING; 392 | } 393 | 394 | if (writeMarker) { 395 | for (int i=0; i exifLocation; 439 | private Map exifMake; 440 | private Map exifDate; 441 | private Map exifSettings; 442 | 443 | public ReducedImage(Uri uri) { 444 | Boolean pro = SendReduced.pro(activity); 445 | mode = pro ? options.getString(Options.PREF_NAME, Options.OPT_NAME_RANDOM) : Options.OPT_NAME_RANDOM; 446 | preserveExifLocation = options.getBoolean(Options.PREF_EXIF_LOCATION, false); 447 | preserveExifMake = options.getBoolean(Options.PREF_EXIF_MAKE_MODEL, false); 448 | preserveExifDate = options.getBoolean(Options.PREF_EXIF_DATETIME, false); 449 | preserveExifSettings = options.getBoolean(Options.PREF_EXIF_SETTINGS, false); 450 | superStrip = pro && options.getBoolean(Options.PREF_SUPER_STRIP, false); 451 | haveExif = pro && ( preserveExifLocation || preserveExifMake || 452 | preserveExifDate || preserveExifSettings ) && ! superStrip; 453 | 454 | bmp = null; 455 | 456 | SendReduced.log("Reducing "+uri); 457 | byte[] data; 458 | try { 459 | data = getStreamBytes(cr.openInputStream(uri)); 460 | } catch (FileNotFoundException e) { 461 | SendReduced.log("error reading: "+e); 462 | return; 463 | } 464 | 465 | if (data == null) 466 | return; 467 | 468 | origDate = -1; 469 | origName = null; 470 | String origPath = null; 471 | 472 | if (uri.getScheme().equalsIgnoreCase("file")) { 473 | origPath = uri.getPath(); 474 | File f = new File(origPath); 475 | origName = f.getName(); 476 | try { 477 | origDate = f.lastModified(); 478 | } 479 | catch(Exception e) {} 480 | } 481 | 482 | if (origDate < 0 || uri.getScheme().equalsIgnoreCase("content")) { 483 | try { 484 | Cursor c = cr.query(uri, null, null, null, null); 485 | if (c != null) { 486 | if (c.moveToFirst()) { 487 | try { 488 | int id = c.getColumnIndex(Images.Media.DATA); 489 | if (id != -1) { 490 | origPath = c.getString(id); 491 | if (origName != null) 492 | origName = new File(origPath).getName(); 493 | } 494 | 495 | if (origDate < 0) { 496 | id = c.getColumnIndex(Images.Media.DATE_ADDED); 497 | SendReduced.log("date id "+id); 498 | if (id != -1) { 499 | origDate = c.getLong(id) * 1000; 500 | } else { 501 | id = c.getColumnIndex(Images.Media.DATE_MODIFIED); 502 | SendReduced.log("date id2 "+id); 503 | if (id != -1) 504 | origDate = c.getLong(id) * 1000; 505 | } 506 | } 507 | } 508 | catch(Exception e) { 509 | SendReduced.log("Error "+e); 510 | } 511 | } 512 | c.close(); 513 | } 514 | } 515 | catch(Exception e) { 516 | SendReduced.log("Error "+e); 517 | } 518 | } 519 | 520 | SendReduced.log("Orig date "+origDate); 521 | if (origName != null) 522 | SendReduced.log("Name "+origName); 523 | SendReduced.log("need to decode "+data.length+" bytes"); 524 | 525 | int orientation = getOrientation(cr, uri); 526 | 527 | InputStream input = new ByteArrayInputStream(data); 528 | 529 | if (haveExif || orientation == INVALID_ROTATION || (origDate < 0 && mode.equals(Options.OPT_NAME_DATE_TIME)) ) { 530 | try { 531 | ExifInterface ei = new ExifInterface(input); 532 | if (orientation == INVALID_ROTATION) { 533 | try { 534 | int o = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); 535 | SendReduced.log("exif orientation " + o); 536 | switch (o) { 537 | case ExifInterface.ORIENTATION_ROTATE_270: 538 | orientation = 270; 539 | break; 540 | case ExifInterface.ORIENTATION_ROTATE_180: 541 | orientation = 180; 542 | break; 543 | case ExifInterface.ORIENTATION_ROTATE_90: 544 | orientation = 90; 545 | break; 546 | case ExifInterface.ORIENTATION_NORMAL: 547 | orientation = 0; 548 | break; 549 | } 550 | } catch (Exception e) { 551 | } 552 | } 553 | 554 | if (origDate < 0) { 555 | String date = ei.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); 556 | if (date == null) { 557 | date = ei.getAttribute(ExifInterface.TAG_DATETIME_DIGITIZED); 558 | if (date == null) { 559 | date = ei.getAttribute(ExifInterface.TAG_DATETIME); 560 | } 561 | } 562 | if (date != null) { 563 | origDate = parseExifDate(date); 564 | } 565 | } 566 | 567 | if (preserveExifDate) 568 | exifDate = getTags(ei, DATETIME_TAGS); 569 | if (preserveExifLocation) 570 | exifLocation = getTags(ei, LOCATION_TAGS); 571 | if (preserveExifMake) 572 | exifMake = getTags(ei, MAKE_TAGS); 573 | if (preserveExifSettings) 574 | exifSettings = getTags(ei, SETTINGS_TAGS); 575 | } 576 | catch (Exception e) { 577 | SendReduced.log("exif " + e); 578 | } 579 | } 580 | 581 | SendReduced.log("orientation = "+orientation); 582 | 583 | Bitmap inBmp = BitmapFactory.decodeByteArray(data, 0, data.length); 584 | 585 | if (inBmp == null) { 586 | SendReduced.log("error decoding"); 587 | return; 588 | } 589 | 590 | int h = inBmp.getHeight(); 591 | int w = inBmp.getWidth(); 592 | 593 | boolean transform = false; 594 | 595 | Matrix m = new Matrix(); 596 | 597 | if (h > outResolution || w > outResolution) { 598 | if (h>w) 599 | m.postScale(outResolution / (float)h, outResolution / (float)h); 600 | else 601 | m.postScale(outResolution / (float)w, outResolution / (float)w); 602 | transform = true; 603 | } 604 | 605 | if (INVALID_ROTATION != orientation && orientation != 0) { 606 | m.postRotate(orientation); 607 | transform = true; 608 | } 609 | 610 | SendReduced.log("image: "+w+"x"+h+" ["+orientation+"]"); 611 | 612 | if (transform) { 613 | bmp = Bitmap.createBitmap(inBmp, 0, 0, w, h, m, true); 614 | if (bmp != inBmp) 615 | inBmp.recycle(); 616 | } 617 | else { 618 | bmp = inBmp; 619 | } 620 | } 621 | 622 | private long parseExifDate(String date) { 623 | SimpleDateFormat format = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); 624 | try { 625 | Date d = format.parse(date); 626 | SendReduced.log("parsing "+date+" to "+d.toString()); 627 | return d.getTime(); 628 | } catch (ParseException e) { 629 | return -1; 630 | } 631 | } 632 | 633 | private Map getTags(ExifInterface ei, 634 | String[] tags) { 635 | Map map = new HashMap(); 636 | for (String tag : tags) 637 | map.put(tag, ei.getAttribute(tag)); 638 | return map; 639 | } 640 | 641 | private void setTags(ExifInterface ei, 642 | Map map) { 643 | if (map == null) 644 | return; 645 | for (String tag : map.keySet()) { 646 | String v = map.get(tag); 647 | if (v != null) 648 | ei.setAttribute(tag, map.get(tag)); 649 | } 650 | } 651 | 652 | File createOutFile() throws IOException { 653 | File outFile = null; 654 | SendReduced.log(mode); 655 | 656 | if (mode.equals(Options.OPT_NAME_SEQUENTIAL)) { 657 | outFile = new File(curCacheDir + "/" + String.format("%04d.jpg", sequencePos)); 658 | sequencePos++; 659 | } 660 | else if (mode.equals(Options.OPT_NAME_DATE_TIME) && origDate > 0) { 661 | Date d = new Date(origDate); 662 | String base = curCacheDir + "/" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(d); 663 | File f = new File(base+".jpg"); 664 | if (! f.exists()) { 665 | outFile = f; 666 | } 667 | else { 668 | for (int i = 2 ; i < 100000 ; i++) { 669 | f = new File(base+"-"+i+".jpg"); 670 | if (! f.exists()) { 671 | outFile = f; 672 | break; 673 | } 674 | } 675 | } 676 | } 677 | else if (mode.equals(Options.OPT_NAME_PRESERVE) && origName != null) { 678 | if (origName.toLowerCase().endsWith(".jpg") || origName.toLowerCase().endsWith(".jpeg")) { 679 | outFile = new File(curCacheDir + "/" + origName); 680 | } 681 | else { 682 | outFile = new File(curCacheDir + "/" + origName + ".jpg"); 683 | } 684 | } 685 | 686 | if (outFile != null) { 687 | SendReduced.log("Trying "+outFile); 688 | try { 689 | if (outFile.createNewFile()) 690 | return outFile; 691 | } catch (IOException e) { 692 | } 693 | } 694 | return File.createTempFile(PREFIX, ".jpg", new File(curCacheDir)); 695 | } 696 | 697 | String saveImage() { 698 | try { 699 | File temp = createOutFile(); //File.createTempFile(PREFIX, ".jpg", storage); 700 | temp.setReadable(true, false); 701 | SendReduced.log("Compressing to "+temp); 702 | OutputStream out = new FileOutputStream(temp); 703 | if (superStrip) 704 | out = new SuperStripStream(out); 705 | boolean status = bmp.compress(Bitmap.CompressFormat.JPEG, outQuality, out); 706 | out.close(); 707 | if (!status) { 708 | temp.delete(); 709 | return null; 710 | } 711 | if (haveExif) { 712 | ExifInterface ei = new ExifInterface(temp.getPath()); 713 | setTags(ei, exifDate); 714 | setTags(ei, exifLocation); 715 | setTags(ei, exifMake); 716 | setTags(ei, exifSettings); 717 | ei.saveAttributes(); 718 | } 719 | return temp.getPath(); 720 | } catch (IOException e) { 721 | SendReduced.log("Error writing "+e); 722 | return null; 723 | } 724 | finally { 725 | bmp.recycle(); 726 | } 727 | 728 | } 729 | 730 | 731 | } 732 | } 733 | -------------------------------------------------------------------------------- /SendReduced/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Send Reduced 4 | 5 | 256 6 | 512 7 | 640 8 | 800 9 | 1024 10 | 1152 11 | 1280 12 | 1536 13 | 2048 14 | 2560 15 | 16 | 17 | 60 18 | 70 19 | 75 20 | 80 21 | 85 22 | 90 23 | 95 24 | 97 25 | 98 26 | 99 27 | 100 28 | 29 | 30 | date and time 31 | preserve 32 | random 33 | sequential 34 | 35 | 36 | -------------------------------------------------------------------------------- /SendReduced/src/main/res/xml/options.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 10 | 16 | 21 | 26 | 31 | 36 | 41 | 42 | 46 | 47 | 53 | 57 | 61 | 65 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /SendReduced/src/pro/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/pro/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/pro/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/pro/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/pro/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/pro/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/pro/res/drawable-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/pro/res/drawable-xhdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/pro/res/drawable-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/SendReduced/src/pro/res/drawable-xxhdpi/icon.png -------------------------------------------------------------------------------- /SendReduced/src/pro/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Send Reduced Pro 4 | 5 | -------------------------------------------------------------------------------- /SendReduced/src/pro/res/xml/paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | buildscript { 5 | repositories { 6 | google() 7 | jcenter() 8 | maven { url "https://maven.google.com" } 9 | mavenCentral() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:4.1.2' 13 | } 14 | } 15 | 16 | allprojects { 17 | repositories { 18 | google() 19 | jcenter() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Apr 24 11:17:59 CDT 2021 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-6.5-bin.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 | -------------------------------------------------------------------------------- /icon114free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/icon114free.png -------------------------------------------------------------------------------- /icon135free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/icon135free.png -------------------------------------------------------------------------------- /icon512free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/icon512free.png -------------------------------------------------------------------------------- /images/Lens_and_wavefronts.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/images/Lens_and_wavefronts.xcf -------------------------------------------------------------------------------- /images/Lens_and_wavefrontsfree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/images/Lens_and_wavefrontsfree.png -------------------------------------------------------------------------------- /images/Lens_and_wavefrontsfree.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 58 | 62 | 66 | 70 | 74 | 75 | 88 | 101 | 114 | 127 | 140 | 153 | 166 | 171 | 176 | 181 | 186 | 191 | 196 | 201 | 206 | 212 | FREE 223 | 224 | -------------------------------------------------------------------------------- /images/Lens_and_wavefrontsfree.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/images/Lens_and_wavefrontsfree.xcf -------------------------------------------------------------------------------- /images/Lens_and_wavefrontspro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arpruss/sendreduced/c3acf2c2ffe260853e822a081ca9ca4c48b01b49/images/Lens_and_wavefrontspro.png -------------------------------------------------------------------------------- /import-summary.txt: -------------------------------------------------------------------------------- 1 | ECLIPSE ANDROID PROJECT IMPORT SUMMARY 2 | ====================================== 3 | 4 | Ignored Files: 5 | -------------- 6 | The following files were *not* copied into the new Gradle project; you 7 | should evaluate whether these are still needed in your project and if 8 | so manually move them: 9 | 10 | * Lens_and_wavefronts.png 11 | * Lens_and_wavefronts.xcf 12 | * Lens_and_wavefrontsfree.png 13 | * Lens_and_wavefrontsfree.svg 14 | * Lens_and_wavefrontsfree.xcf 15 | * icon114.png 16 | * icon135.png 17 | * icon512.png 18 | * license.txt 19 | * scale.sh 20 | * toNormal.sh 21 | * toPro.sh 22 | 23 | Moved Files: 24 | ------------ 25 | Android Gradle projects use a different directory structure than ADT 26 | Eclipse projects. Here's how the projects were restructured: 27 | 28 | * AndroidManifest.xml => app\src\main\AndroidManifest.xml 29 | * assets\ => app\src\main\assets\ 30 | * lint.xml => app\lint.xml 31 | * res\ => app\src\main\res\ 32 | * src\ => app\src\main\java\ 33 | 34 | Next Steps: 35 | ----------- 36 | You can now build the project. The Gradle project needs network 37 | connectivity to download dependencies. 38 | 39 | Bugs: 40 | ----- 41 | If for some reason your project does not build, and you determine that 42 | it is due to a bug or limitation of the Eclipse to Gradle importer, 43 | please file a bug at http://b.android.com with category 44 | Component-Tools. 45 | 46 | (This import summary is for your information only, and can be deleted 47 | after import once you are satisfied with the results.) 48 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | FileProvider.java Copyright (C) 2013 The Android Open Source Project and/or Google, Inc. 2 | Rest of code Copyright (C) 2011-2016 Omega Centauri Software 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright [yyyy] [name of copyright owner] 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # 6 | #Sat Apr 24 11:17:59 CDT 2021 7 | sdk.dir=C\:\\Users\\Alexander\\AppData\\Local\\Android\\Sdk 8 | -------------------------------------------------------------------------------- /scale.sh: -------------------------------------------------------------------------------- 1 | R=SendReduced/src/$1/res 2 | convert images/Lens_and_wavefronts$1.png -resize 114x114 icon114$1.png 3 | convert images/Lens_and_wavefronts$1.png -resize 135x135 icon135$1.png 4 | convert images/Lens_and_wavefronts$1.png -resize 512x512 icon512$1.png 5 | convert images/Lens_and_wavefronts$1.png -resize 144x144 $R/drawable-xxhdpi/icon.png 6 | convert images/Lens_and_wavefronts$1.png -resize 96x96 $R/drawable-xhdpi/icon.png 7 | convert images/Lens_and_wavefronts$1.png -resize 72x72 $R/drawable-hdpi/icon.png 8 | convert images/Lens_and_wavefronts$1.png -resize 72x72 $R/drawable-hdpi/icon.png 9 | convert images/Lens_and_wavefronts$1.png -resize 48x48 $R/drawable-mdpi/icon.png 10 | convert images/Lens_and_wavefronts$1.png -resize 32x32 $R/drawable-ldpi/icon.png 11 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':SendReduced' 2 | --------------------------------------------------------------------------------