Compare commits
10 commits
b8dbee59ad
...
5c2d7f241e
Author | SHA1 | Date | |
---|---|---|---|
5c2d7f241e | |||
c588df413b | |||
d5afbdb037 | |||
5aa56d9f5c | |||
dc03b2fb46 | |||
bd0af0a8a9 | |||
91e1f22f86 | |||
81f421b9c0 | |||
2787aee01a | |||
f83bf386cd |
12 changed files with 534 additions and 108 deletions
82
README.md
82
README.md
|
@ -1,8 +1,15 @@
|
|||
# Android App (build via docker/podman)
|
||||
# Android App to Test FileProvider
|
||||
|
||||
A "FileProvider" is a special type of "ContentProvider"
|
||||
|
||||
The goal of this repo, is to create a container that can serve to produce an "empty android Application" (i.e `app.apk` file).
|
||||
As such the philosophy is to keep the process "simple" as to now make the understanding too difficult.
|
||||
|
||||
The Code regarding the FileProvider setup was adopted from
|
||||
https://github.com/commonsguy/cw-omnibus/blob/master/ContentProvider/Files/app/src/main/java/com/commonsware/android/cp/files/AbstractFileProvider.java
|
||||
and the copyright notice was retained in the files
|
||||
|
||||
|
||||
## usage
|
||||
|
||||
0. clone this repo
|
||||
|
@ -18,76 +25,3 @@ As such the philosophy is to keep the process "simple" as to now make the unders
|
|||
adb install -r app/result/app.apk
|
||||
```
|
||||
|
||||
## basic ideas
|
||||
|
||||
* work within container (debian based image)
|
||||
* use Makefile as a build tool
|
||||
|
||||
## benefits of this (compared to AndroidStudio)
|
||||
|
||||
* no need to install rather bloated hell of software (i.e AndroidStudio) and all
|
||||
* less hidding of internals (i.e Makefile allows to see how app.apk is made)
|
||||
* small app.apk file
|
||||
* oftentimes faster compile time (as compared with AndroidStudio Gradle builds)
|
||||
* quick "webview" which can serve as starting point for people that can to PWA and websites
|
||||
* Assisted initial configuration provides access to configure almost all types of [Android app permissions](app/.Makefile.scripts/make--app-config.sh#L127)
|
||||
* no need to have Kotlin, Gradle setup
|
||||
/
|
||||
## basic info
|
||||
|
||||
This repo should allow to generate an empty "android app". by simply cloning this repo and
|
||||
```
|
||||
./build-android-app.sh
|
||||
```
|
||||
It does so via:
|
||||
|
||||
1. building a container (in any of the runtime/daemons it finds: i.e. docker,podman,etc..)
|
||||
2. running this container having the `./app` folder being mounted within as `/app`
|
||||
3. executing the [`app/Makefile`](app/Makefile) which will then:
|
||||
4. either work with the configuration stored in an `app/app-config.sh` in case such file exists or
|
||||
5. if not go through a `whiptail` text menu wizzard to configure a new empty app. (Makefile recipe: `./app-config.sh`)
|
||||
6. it will then download the required android sdk files as necessary (Makefile recipe: `./android-sdk/installed`)
|
||||
7. go through the further steps to setup the blank app.
|
||||
|
||||
|
||||
|
||||
## files and purpose
|
||||
|
||||
Upon `clone` of this repo the `app` folder is setup with these files:
|
||||
```
|
||||
# The (GNU) Makefile which...
|
||||
app/Makefile
|
||||
# ... has recipes that call scripts in folder....
|
||||
app/.Makefile.scripts
|
||||
# .. which creates an `app-config.sh`, a file to keep
|
||||
# the configuration (app name,lable,api-levels,permissions etc)...
|
||||
app/.Makefile.scripts/make--app-config.sh
|
||||
# .. which creates an `AndroidManifest.xml`
|
||||
app/.Makefile.scripts/make--AndroidManifest.xml
|
||||
# .. which creates an `AppActivity.java` file (which just setup a Webview and loads `assets/index.html`)
|
||||
app/.Makefile.scripts/make--AppActivity.java.sh
|
||||
# .. which installs the necessary Android SDK in the correct versions
|
||||
app/.Makefile.scripts/make--android-sdk.sh
|
||||
app/assets
|
||||
# the index.html file
|
||||
app/assets/index.html
|
||||
app/res
|
||||
app/res/drawable
|
||||
# the icon of the app
|
||||
app/res/drawable/appicon.xml
|
||||
```
|
||||
|
||||
Upon further `./build-android-app.sh` execution more folders will appear
|
||||
```
|
||||
# a folder in which the Android-sdk stuff (installed via sdkmanager) is stored
|
||||
android-sdk
|
||||
# folders used during build...
|
||||
# ... for temporary files
|
||||
bin/
|
||||
obj/
|
||||
result/
|
||||
# app configuration resulting from text whiptail menu
|
||||
app-config.sh
|
||||
# the Manifest file as resulted from data from app-config.sh
|
||||
AndroidManifest.xml
|
||||
```
|
||||
|
|
|
@ -8,31 +8,21 @@ test -f app-config.sh && {
|
|||
echo "package $APP_PACKAGE;"
|
||||
|
||||
cat << 'APPACTIVITYJAVA'
|
||||
|
||||
import android.provider.Settings ;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.util.Base64;
|
||||
import java.util.Objects;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.CountDownTimer;
|
||||
import android.os.Environment;
|
||||
import android.text.method.ScrollingMovementMethod;
|
||||
import android.view.*;
|
||||
//import android.view.MenuItem;
|
||||
import android.view.ViewGroup.*;
|
||||
import android.widget.*;
|
||||
//import android.widget.Toast;
|
||||
//import android.widget.TextView;
|
||||
import android.view.*;
|
||||
// for WebView,WebMessage,WebMessagePort,
|
||||
import android.webkit.*;
|
||||
import android.net.Uri;
|
||||
import org.json.JSONObject;
|
||||
//import android.webkit.WebView;
|
||||
//import android.webkit.WebMessage;
|
||||
//import android.webkit.WebMessagePort;
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
|
||||
|
@ -70,6 +60,9 @@ public class AppActivity extends Activity {
|
|||
myWebSettings.setBuiltInZoomControls(true);
|
||||
myWebSettings.setDisplayZoomControls(false);
|
||||
myWebSettings.setJavaScriptEnabled(true);
|
||||
myWebSettings.setDomStorageEnabled(true);
|
||||
myWebSettings.setDatabaseEnabled(true);
|
||||
myWebSettings.setDatabasePath("/data/data/" + myWebView.getContext().getPackageName() + "/databases/");
|
||||
myWebView.addJavascriptInterface(this, "myJavaScriptInterface");
|
||||
// load the html from assets file
|
||||
String html = readFileFromAssets("index.html");
|
||||
|
@ -85,4 +78,3 @@ public class AppActivity extends Activity {
|
|||
return "this is good";
|
||||
}
|
||||
}
|
||||
APPACTIVITYJAVA
|
||||
|
|
32
app/AndroidManifest.xml
Normal file
32
app/AndroidManifest.xml
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.testfileprovider"
|
||||
android:versionCode="1"
|
||||
android:versionName="1.0">
|
||||
<uses-sdk android:minSdkVersion="30"
|
||||
android:targetSdkVersion="33"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<application android:debuggable="true" android:label="FileProvider" android:icon="@drawable/appicon">
|
||||
<provider
|
||||
android:name="app.testfileprovider.FileProvider"
|
||||
android:authorities="app.testfileprovider"
|
||||
android:exported="true"
|
||||
/>
|
||||
<activity android:name="app.testfileprovider.AppActivity"
|
||||
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="FileProvider">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
19
app/Makefile
19
app/Makefile
|
@ -28,23 +28,25 @@ include Makefile.app-config
|
|||
-storepass armena -keypass armena -alias helljniKey -keyalg RSA -v
|
||||
|
||||
# aapt "package" together the dalvik/hex stuff (and "assets" and "res")
|
||||
./result/unsigned.apk : ./result/bin/classes.dex ./assets ./AndroidManifest.xml
|
||||
./result/unsigned.apk : ./result/bin/classes.dex $(shell find ./assets -type f) ./assets ./AndroidManifest.xml
|
||||
rm -rvf "$@"
|
||||
$(BUILDTOOLS)/aapt package \
|
||||
-v -u -f -M ./AndroidManifest.xml -S ./res \
|
||||
-I $(ANDROID_JAR) -A ./assets -F $@ ./result/bin
|
||||
|
||||
# convert "java class"es files (i.e bytecode) to dalvic/d8 android thing
|
||||
./result/bin/classes.dex : ./obj/$(PACKAGE)/AppActivity.class
|
||||
./result/bin/classes.dex : ./result/last.javac
|
||||
mkdir -p ./result/bin
|
||||
$(BUILDTOOLS)/d8 ./obj/$(PACKAGE)/*.class \
|
||||
--lib $(ANDROID_JAR) --output ./result/bin
|
||||
|
||||
# compile (javac) the class from
|
||||
./obj/$(PACKAGE)/AppActivity.class : ./src/$(PACKAGE)/AppActivity.java ./src/$(PACKAGE)/R.java
|
||||
mkdir -p ./obj/$(PACKAGE)
|
||||
javac -d ./obj -classpath $(ANDROID_JAR) -sourcepath ./src $<
|
||||
|
||||
# compile (javac) the class from
|
||||
#./obj/$(PACKAGE)/AppActivity.class : ./src/$(PACKAGE)/AppActivity.java ./src/$(PACKAGE)/*.java
|
||||
./result/last.javac : ./src/$(PACKAGE)/AppActivity.java ./src/$(PACKAGE)/*.java
|
||||
mkdir -p ./result
|
||||
mkdir -p ./obj/$(PACKAGE)
|
||||
javac -d ./obj -classpath $(ANDROID_JAR) -sourcepath ./src $? | tee $@
|
||||
|
||||
# make the resources "R.java" thing
|
||||
./src/$(PACKAGE)/R.java : $(shell find ./res -type f) app-config.sh ./AndroidManifest.xml ./android-sdk/installed | ./src/$(PACKAGE)
|
||||
|
@ -61,13 +63,14 @@ include Makefile.app-config
|
|||
mkdir -p $@
|
||||
|
||||
# install the necessary android sdks
|
||||
./android-sdk/installed: app-config.sh
|
||||
./android-sdk/installed: app-config.sh ./.Makefile.scripts/make--android-sdk.sh
|
||||
./.Makefile.scripts/make--android-sdk.sh
|
||||
|
||||
# generate the AndroidManifest.xml
|
||||
./AndroidManifest.xml: app-config.sh
|
||||
./AndroidManifest.xml: app-config.sh ./.Makefile.scripts/make--AndroidManifest.xml
|
||||
./.Makefile.scripts/make--AndroidManifest.xml
|
||||
|
||||
# !!this step (when/if) run will trigger a restart of the "make" as the rules tartget is included
|
||||
Makefile.app-config: app-config.sh Makefile
|
||||
source app-config.sh; \
|
||||
tee $@ << MAKEFILE_APP_CONFIG
|
||||
|
|
BIN
app/assets/foto.jpg
Normal file
BIN
app/assets/foto.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
|
@ -15,9 +15,16 @@
|
|||
opacity:1.0
|
||||
}
|
||||
}
|
||||
button {
|
||||
font-size:14mm;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Webview</h1>
|
||||
<h2> clicking those buttons starts <a href="https://developer.android.com/reference/android/content/Intent">intents</a> to open those files</h2>
|
||||
<button onclick="myJavaScriptInterface.startActivity('foto.jpg')">foto.jpg</button><br>
|
||||
<button onclick="myJavaScriptInterface.startActivity('test.pdf')">test.pdf</button><br>
|
||||
<button onclick="myJavaScriptInterface.startActivity('index.html')">index.html</button><br>
|
||||
<script>
|
||||
window.addEventListener("error",(error)=>{
|
||||
document.body.innerHTML = "<h1>error</h1><pre>" +
|
||||
|
@ -25,16 +32,6 @@ window.addEventListener("error",(error)=>{
|
|||
"\nline:" + error.lineno +
|
||||
"\n"+error.message +"</pre>";
|
||||
},false);
|
||||
|
||||
window.addEventListener("load",()=>{
|
||||
var count = localStorage.getItem("app-opened-count")|| 0;
|
||||
count++;
|
||||
localStorage.setItem("app-opened-count",count);
|
||||
var h2 = document.createElement("h2");
|
||||
h2.textContent = "Javascript works! (app was opened " + count + " times)";
|
||||
h2.style.animation="wobble 1s ease-in-out 0s 1 forwards normal running"
|
||||
document.body.appendChild(h2);
|
||||
},false);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
BIN
app/assets/test.pdf
Normal file
BIN
app/assets/test.pdf
Normal file
Binary file not shown.
|
@ -5,5 +5,5 @@
|
|||
android:height="100dp">
|
||||
<path
|
||||
android:pathData="M95 50A45 45 0 0 1 5 50A45 45 0 0 1 95 50Z"
|
||||
android:fillColor="#FF0000" />
|
||||
android:fillColor="#00AA00" />
|
||||
</vector>
|
||||
|
|
86
app/src/app/testfileprovider/AppActivity.java
Normal file
86
app/src/app/testfileprovider/AppActivity.java
Normal file
|
@ -0,0 +1,86 @@
|
|||
package app.testfileprovider;
|
||||
import android.provider.Settings ;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
//import android.view.MenuItem;
|
||||
import android.view.*;
|
||||
// for WebView,WebMessage,WebMessagePort,
|
||||
import android.webkit.*;
|
||||
import android.net.Uri;
|
||||
import org.json.JSONObject;
|
||||
import java.io.InputStream;
|
||||
import java.io.File;
|
||||
import java.util.Objects;
|
||||
|
||||
|
||||
public class AppActivity extends Activity {
|
||||
|
||||
private static final String BASE_URI = "https://alexmahr.de";
|
||||
|
||||
public String readFileFromAssets(String filename) {
|
||||
String filecontents = "";
|
||||
try {
|
||||
InputStream stream = getAssets().open(filename);
|
||||
int filesize = stream.available();
|
||||
byte[] filebuffer = new byte[filesize];
|
||||
stream.read(filebuffer);
|
||||
stream.close();
|
||||
filecontents = new String(filebuffer);
|
||||
} catch (Exception e) {
|
||||
// I <3 java exceptions
|
||||
}
|
||||
return filecontents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
// this removes the title bar (a ~1cm big strip at the top of the app showing its name
|
||||
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
// we create the webview (at least on the android 14 that is a webview that features avif + websockets etc....)
|
||||
WebView myWebView = new WebView(this);//activityContext);
|
||||
// MyJavascriptInterface myJavaScriptInterface = new MyJavascriptInterface(this,myWebView);
|
||||
// we create a webview (there is also setwebviewclient-vs-setwebchromeclient
|
||||
WebViewClient myWebViewClient= new WebViewClient();
|
||||
myWebView.setWebViewClient(myWebViewClient);
|
||||
// to setup settings
|
||||
WebSettings myWebSettings = myWebView.getSettings();
|
||||
myWebSettings.setBuiltInZoomControls(true);
|
||||
myWebSettings.setDisplayZoomControls(false);
|
||||
myWebSettings.setJavaScriptEnabled(true);
|
||||
myWebSettings.setDomStorageEnabled(true);
|
||||
myWebSettings.setDatabaseEnabled(true);
|
||||
myWebSettings.setDatabasePath("/data/data/" + myWebView.getContext().getPackageName() + "/databases/");
|
||||
myWebView.addJavascriptInterface(this, "myJavaScriptInterface");
|
||||
// load the html from assets file
|
||||
String html = readFileFromAssets("index.html");
|
||||
myWebView.loadDataWithBaseURL(BASE_URI,html, "text/html", "UTF-8",null);
|
||||
//myWebView.loadData(encodedHtml, "text/html", "base64");
|
||||
// alternatively this could be to load a website
|
||||
//myWebView.loadUrl("https://alexmahr.de/ru");
|
||||
setContentView(myWebView);
|
||||
}
|
||||
|
||||
|
||||
@JavascriptInterface
|
||||
public String startActivity(String fileName) {
|
||||
File filesDir = getFilesDir();
|
||||
Log.i("startActivity with Uri",FileProvider.CONTENT_URI
|
||||
+ fileName);
|
||||
Intent i = new Intent(Intent.ACTION_VIEW,
|
||||
Uri.parse(FileProvider.CONTENT_URI
|
||||
+ fileName));
|
||||
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
startActivity(i);
|
||||
return filesDir.getAbsolutePath();
|
||||
}
|
||||
|
||||
|
||||
@JavascriptInterface
|
||||
public String toString() {
|
||||
return "this is good";
|
||||
}
|
||||
}
|
158
app/src/app/testfileprovider/FileProvider.java
Normal file
158
app/src/app/testfileprovider/FileProvider.java
Normal file
|
@ -0,0 +1,158 @@
|
|||
/***
|
||||
Copyright (c) 2008-2014 CommonsWare, LLC
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
use this file except in compliance with the License. You may obtain a copy
|
||||
of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required
|
||||
by applicable law or agreed to in writing, software distributed under the
|
||||
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
OF ANY KIND, either express or implied. See the License for the specific
|
||||
language governing permissions and limitations under the License.
|
||||
|
||||
Covered in detail in the book _The Busy Coder's Guide to Android Development_
|
||||
https://commonsware.com/Android
|
||||
*/
|
||||
|
||||
package app.testfileprovider;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.util.Log;
|
||||
// via local java file
|
||||
// copied from https://github.com/MuntashirAkon/cwac-provider/blob/3dccc0ef9cd2eda5fe149b87ff4ef4881c583f8c/provider/src/main/java/com/commonsware/cwac/provider/LegacyCompatCursorWrapper.java
|
||||
//import com.commonsware.cwac.provider.LegacyCompatCursorWrapper;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLConnection;
|
||||
|
||||
public class FileProvider extends ContentProvider {
|
||||
private final static String[] OPENABLE_PROJECTION= {
|
||||
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
|
||||
public static final Uri CONTENT_URI=
|
||||
Uri.parse("content://app.testfileprovider/");
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return(true);
|
||||
}
|
||||
|
||||
public File openAssetFile(String path)
|
||||
throws FileNotFoundException {
|
||||
File root=getContext().getFilesDir();
|
||||
File f = new File(root, path).getAbsoluteFile();
|
||||
if(!f.exists()){
|
||||
AssetManager assets=getContext().getAssets();
|
||||
try {
|
||||
copy(assets.open(path.substring(1)), f);
|
||||
}
|
||||
catch (IOException e) {
|
||||
Log.e("FileProvider", "Exception copying from assets", e);
|
||||
throw new FileNotFoundException(path);
|
||||
}
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
File root=getContext().getFilesDir();
|
||||
File f = openAssetFile(uri.getPath());
|
||||
|
||||
if (!f.getPath().startsWith(root.getPath())) {
|
||||
throw new
|
||||
SecurityException("Resolved path jumped beyond root");
|
||||
}
|
||||
|
||||
if (f.exists()) {
|
||||
return(ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY));
|
||||
}
|
||||
|
||||
throw new FileNotFoundException(uri.getPath());
|
||||
}
|
||||
|
||||
protected long getDataLength(Uri uri)
|
||||
{
|
||||
try {
|
||||
File f = openAssetFile(uri.getPath());
|
||||
return(f.length());
|
||||
} catch (Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected String getFileName(Uri uri) {
|
||||
return(uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return(URLConnection.guessContentTypeFromName(uri.toString()));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues initialValues) {
|
||||
throw new RuntimeException("Operation not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where,
|
||||
String[] whereArgs) {
|
||||
throw new RuntimeException("Operation not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String where, String[] whereArgs) {
|
||||
throw new RuntimeException("Operation not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
if (projection == null) {
|
||||
projection=OPENABLE_PROJECTION;
|
||||
}
|
||||
|
||||
final MatrixCursor cursor=new MatrixCursor(projection, 1);
|
||||
|
||||
MatrixCursor.RowBuilder b=cursor.newRow();
|
||||
|
||||
for (String col : projection) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||
b.add(getFileName(uri));
|
||||
}
|
||||
else if (OpenableColumns.SIZE.equals(col)) {
|
||||
b.add(getDataLength(uri));
|
||||
}
|
||||
else { // unknown, so just add null
|
||||
b.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
return(new LegacyCompatCursorWrapper(cursor));
|
||||
}
|
||||
|
||||
static void copy(InputStream in, File dst)
|
||||
throws IOException {
|
||||
FileOutputStream out=new FileOutputStream(dst);
|
||||
byte[] buf=new byte[1024];
|
||||
int len;
|
||||
|
||||
while ((len=in.read(buf)) >= 0) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
|
||||
in.close();
|
||||
out.close();
|
||||
}
|
||||
}
|
219
app/src/app/testfileprovider/LegacyCompatCursorWrapper.java
Normal file
219
app/src/app/testfileprovider/LegacyCompatCursorWrapper.java
Normal file
|
@ -0,0 +1,219 @@
|
|||
/***
|
||||
Copyright (c) 2015-2016 CommonsWare, LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package app.testfileprovider;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.net.Uri;
|
||||
import java.util.Arrays;
|
||||
import static android.provider.MediaStore.MediaColumns.DATA;
|
||||
import static android.provider.MediaStore.MediaColumns.MIME_TYPE;
|
||||
|
||||
/**
|
||||
* Wraps the Cursor returned by an ordinary FileProvider,
|
||||
* StreamProvider, or other ContentProvider. If the query()
|
||||
* requests _DATA or MIME_TYPE, adds in some values for
|
||||
* that column, so the client getting this Cursor is less
|
||||
* likely to crash. Of course, clients should not be requesting
|
||||
* either of these columns in the first place...
|
||||
*/
|
||||
public class LegacyCompatCursorWrapper extends CursorWrapper {
|
||||
final private int fakeDataColumn;
|
||||
final private int fakeMimeTypeColumn;
|
||||
final private String mimeType;
|
||||
final private Uri uriForDataColumn;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param cursor the Cursor to be wrapped
|
||||
*/
|
||||
public LegacyCompatCursorWrapper(Cursor cursor) {
|
||||
this(cursor, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param cursor the Cursor to be wrapped
|
||||
* @param mimeType the MIME type of the content represented
|
||||
* by the Uri that generated this Cursor, should
|
||||
* we need it
|
||||
*/
|
||||
public LegacyCompatCursorWrapper(Cursor cursor, String mimeType) {
|
||||
this(cursor, mimeType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param cursor the Cursor to be wrapped
|
||||
* @param mimeType the MIME type of the content represented
|
||||
* by the Uri that generated this Cursor, should
|
||||
* we need it
|
||||
* @param uriForDataColumn Uri to return for the _DATA column
|
||||
*/
|
||||
public LegacyCompatCursorWrapper(Cursor cursor, String mimeType,
|
||||
Uri uriForDataColumn) {
|
||||
super(cursor);
|
||||
|
||||
this.uriForDataColumn=uriForDataColumn;
|
||||
|
||||
if (cursor.getColumnIndex(DATA)>=0) {
|
||||
fakeDataColumn=-1;
|
||||
}
|
||||
else {
|
||||
fakeDataColumn=cursor.getColumnCount();
|
||||
}
|
||||
|
||||
if (cursor.getColumnIndex(MIME_TYPE)>=0) {
|
||||
fakeMimeTypeColumn=-1;
|
||||
}
|
||||
else if (fakeDataColumn==-1) {
|
||||
fakeMimeTypeColumn=cursor.getColumnCount();
|
||||
}
|
||||
else {
|
||||
fakeMimeTypeColumn=fakeDataColumn+1;
|
||||
}
|
||||
|
||||
this.mimeType=mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
int count=super.getColumnCount();
|
||||
|
||||
if (!cursorHasDataColumn()) {
|
||||
count+=1;
|
||||
}
|
||||
|
||||
if (!cursorHasMimeTypeColumn()) {
|
||||
count+=1;
|
||||
}
|
||||
|
||||
return(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int getColumnIndex(String columnName) {
|
||||
if (!cursorHasDataColumn() && DATA.equalsIgnoreCase(
|
||||
columnName)) {
|
||||
return(fakeDataColumn);
|
||||
}
|
||||
|
||||
if (!cursorHasMimeTypeColumn() && MIME_TYPE.equalsIgnoreCase(
|
||||
columnName)) {
|
||||
return(fakeMimeTypeColumn);
|
||||
}
|
||||
|
||||
return(super.getColumnIndex(columnName));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getColumnName(int columnIndex) {
|
||||
if (columnIndex==fakeDataColumn) {
|
||||
return(DATA);
|
||||
}
|
||||
|
||||
if (columnIndex==fakeMimeTypeColumn) {
|
||||
return(MIME_TYPE);
|
||||
}
|
||||
|
||||
return(super.getColumnName(columnIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String[] getColumnNames() {
|
||||
if (cursorHasDataColumn() && cursorHasMimeTypeColumn()) {
|
||||
return(super.getColumnNames());
|
||||
}
|
||||
|
||||
String[] orig=super.getColumnNames();
|
||||
String[] result=Arrays.copyOf(orig, getColumnCount());
|
||||
|
||||
if (!cursorHasDataColumn()) {
|
||||
result[fakeDataColumn]=DATA;
|
||||
}
|
||||
|
||||
if (!cursorHasMimeTypeColumn()) {
|
||||
result[fakeMimeTypeColumn]=MIME_TYPE;
|
||||
}
|
||||
|
||||
return(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String getString(int columnIndex) {
|
||||
if (!cursorHasDataColumn() && columnIndex==fakeDataColumn) {
|
||||
if (uriForDataColumn!=null) {
|
||||
return(uriForDataColumn.toString());
|
||||
}
|
||||
|
||||
return(null);
|
||||
}
|
||||
|
||||
if (!cursorHasMimeTypeColumn() && columnIndex==fakeMimeTypeColumn) {
|
||||
return(mimeType);
|
||||
}
|
||||
|
||||
return(super.getString(columnIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public int getType(int columnIndex) {
|
||||
if (!cursorHasDataColumn() && columnIndex==fakeDataColumn) {
|
||||
return(Cursor.FIELD_TYPE_STRING);
|
||||
}
|
||||
|
||||
if (!cursorHasMimeTypeColumn() && columnIndex==fakeMimeTypeColumn) {
|
||||
return(Cursor.FIELD_TYPE_STRING);
|
||||
}
|
||||
|
||||
return(super.getType(columnIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the Cursor has a _DATA column, false otherwise
|
||||
*/
|
||||
private boolean cursorHasDataColumn() {
|
||||
return(fakeDataColumn==-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the Cursor has a MIME_TYPE column, false
|
||||
* otherwise
|
||||
*/
|
||||
private boolean cursorHasMimeTypeColumn() {
|
||||
return(fakeMimeTypeColumn==-1);
|
||||
}
|
||||
}
|
|
@ -36,7 +36,11 @@ RUN <<EOF
|
|||
cat > /bin/makefile-bash-wrapper.sh << 'WRAPPER'
|
||||
#!/bin/bash
|
||||
printf $'\033[0;32m''#----------------------------------------\n'$'\033[0m' >&2
|
||||
bash "$@"
|
||||
bash -eo pipefail "$@" || {
|
||||
EXITCODE=$?
|
||||
printf $'\033[0;31m''ERROR EXITCODE='"$EXITCODE"'\n'$'\033[0m' >&2
|
||||
exit $EXITCODE
|
||||
}
|
||||
printf '\n\n\n\n' >&2
|
||||
WRAPPER
|
||||
chmod u+x /bin/makefile-bash-wrapper.sh
|
||||
|
@ -44,6 +48,7 @@ EOF
|
|||
DOCKERFILEEOF
|
||||
}
|
||||
|
||||
printf $'\033[0;33m'"$(date -Iseconds) starting build"'$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\n'$'\033[0m'
|
||||
diff Dockerfile <(DockerfileContent) 2>/dev/null > /dev/null || {
|
||||
test -f Dockerfile && {
|
||||
read -p 'reset/start Dockerfile[Y/n]' YES
|
||||
|
|
Loading…
Add table
Reference in a new issue