Compare commits

...

10 commits

12 changed files with 534 additions and 108 deletions

View file

@ -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). 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. 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 ## usage
0. clone this repo 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 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
```

View file

@ -8,31 +8,21 @@ test -f app-config.sh && {
echo "package $APP_PACKAGE;" echo "package $APP_PACKAGE;"
cat << 'APPACTIVITYJAVA' cat << 'APPACTIVITYJAVA'
import android.provider.Settings ; import android.provider.Settings ;
import android.content.Intent; import android.content.Intent;
import android.util.Log; import android.util.Log;
import android.util.Base64;
import java.util.Objects;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Environment; import android.os.Environment;
import android.text.method.ScrollingMovementMethod;
import android.view.*;
//import android.view.MenuItem; //import android.view.MenuItem;
import android.view.ViewGroup.*; import android.view.*;
import android.widget.*; // for WebView,WebMessage,WebMessagePort,
//import android.widget.Toast;
//import android.widget.TextView;
import android.webkit.*; import android.webkit.*;
import android.net.Uri; import android.net.Uri;
import org.json.JSONObject; import org.json.JSONObject;
//import android.webkit.WebView;
//import android.webkit.WebMessage;
//import android.webkit.WebMessagePort;
import java.io.InputStream; import java.io.InputStream;
import java.io.File; import java.io.File;
import java.util.Objects;
@ -70,6 +60,9 @@ public class AppActivity extends Activity {
myWebSettings.setBuiltInZoomControls(true); myWebSettings.setBuiltInZoomControls(true);
myWebSettings.setDisplayZoomControls(false); myWebSettings.setDisplayZoomControls(false);
myWebSettings.setJavaScriptEnabled(true); myWebSettings.setJavaScriptEnabled(true);
myWebSettings.setDomStorageEnabled(true);
myWebSettings.setDatabaseEnabled(true);
myWebSettings.setDatabasePath("/data/data/" + myWebView.getContext().getPackageName() + "/databases/");
myWebView.addJavascriptInterface(this, "myJavaScriptInterface"); myWebView.addJavascriptInterface(this, "myJavaScriptInterface");
// load the html from assets file // load the html from assets file
String html = readFileFromAssets("index.html"); String html = readFileFromAssets("index.html");
@ -85,4 +78,3 @@ public class AppActivity extends Activity {
return "this is good"; return "this is good";
} }
} }
APPACTIVITYJAVA

32
app/AndroidManifest.xml Normal file
View 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>

View file

@ -28,23 +28,25 @@ include Makefile.app-config
-storepass armena -keypass armena -alias helljniKey -keyalg RSA -v -storepass armena -keypass armena -alias helljniKey -keyalg RSA -v
# aapt "package" together the dalvik/hex stuff (and "assets" and "res") # 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 "$@" rm -rvf "$@"
$(BUILDTOOLS)/aapt package \ $(BUILDTOOLS)/aapt package \
-v -u -f -M ./AndroidManifest.xml -S ./res \ -v -u -f -M ./AndroidManifest.xml -S ./res \
-I $(ANDROID_JAR) -A ./assets -F $@ ./result/bin -I $(ANDROID_JAR) -A ./assets -F $@ ./result/bin
# convert "java class"es files (i.e bytecode) to dalvic/d8 android thing # 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 mkdir -p ./result/bin
$(BUILDTOOLS)/d8 ./obj/$(PACKAGE)/*.class \ $(BUILDTOOLS)/d8 ./obj/$(PACKAGE)/*.class \
--lib $(ANDROID_JAR) --output ./result/bin --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 # 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) ./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 $@ mkdir -p $@
# install the necessary android sdks # 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 ./.Makefile.scripts/make--android-sdk.sh
# generate the AndroidManifest.xml # generate the AndroidManifest.xml
./AndroidManifest.xml: app-config.sh ./AndroidManifest.xml: app-config.sh ./.Makefile.scripts/make--AndroidManifest.xml
./.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 Makefile.app-config: app-config.sh Makefile
source app-config.sh; \ source app-config.sh; \
tee $@ << MAKEFILE_APP_CONFIG tee $@ << MAKEFILE_APP_CONFIG

BIN
app/assets/foto.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -15,9 +15,16 @@
opacity:1.0 opacity:1.0
} }
} }
button {
font-size:14mm;
}
</style> </style>
<body> <body>
<h1>Webview</h1> <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> <script>
window.addEventListener("error",(error)=>{ window.addEventListener("error",(error)=>{
document.body.innerHTML = "<h1>error</h1><pre>" + document.body.innerHTML = "<h1>error</h1><pre>" +
@ -25,16 +32,6 @@ window.addEventListener("error",(error)=>{
"\nline:" + error.lineno + "\nline:" + error.lineno +
"\n"+error.message +"</pre>"; "\n"+error.message +"</pre>";
},false); },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> </script>
</body> </body>
</html> </html>

BIN
app/assets/test.pdf Normal file

Binary file not shown.

View file

@ -5,5 +5,5 @@
android:height="100dp"> android:height="100dp">
<path <path
android:pathData="M95 50A45 45 0 0 1 5 50A45 45 0 0 1 95 50Z" android:pathData="M95 50A45 45 0 0 1 5 50A45 45 0 0 1 95 50Z"
android:fillColor="#FF0000" /> android:fillColor="#00AA00" />
</vector> </vector>

View 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";
}
}

View 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();
}
}

View 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);
}
}

View file

@ -36,7 +36,11 @@ RUN <<EOF
cat > /bin/makefile-bash-wrapper.sh << 'WRAPPER' cat > /bin/makefile-bash-wrapper.sh << 'WRAPPER'
#!/bin/bash #!/bin/bash
printf $'\033[0;32m''#----------------------------------------\n'$'\033[0m' >&2 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 printf '\n\n\n\n' >&2
WRAPPER WRAPPER
chmod u+x /bin/makefile-bash-wrapper.sh chmod u+x /bin/makefile-bash-wrapper.sh
@ -44,6 +48,7 @@ EOF
DOCKERFILEEOF DOCKERFILEEOF
} }
printf $'\033[0;33m'"$(date -Iseconds) starting build"'$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$\n'$'\033[0m'
diff Dockerfile <(DockerfileContent) 2>/dev/null > /dev/null || { diff Dockerfile <(DockerfileContent) 2>/dev/null > /dev/null || {
test -f Dockerfile && { test -f Dockerfile && {
read -p 'reset/start Dockerfile[Y/n]' YES read -p 'reset/start Dockerfile[Y/n]' YES