Hi all, in my LibGDX application after changing my target Android API to 26 my app is crashing when writing using an external FileHandle. I had this working when targeting a lower API (20) by adding these to my AndroidManifest.xml:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
But a few months Google changed the minimum API to 26, which requires handling runtime user permissions so the AndroidManifest entry is no longer enough to make it work.
I have no experience with Android specific code because everything I’ve needed to do up to this point I have been able to do entirely within LibGDX. Here is a tutorial I found for external read permission in Android, but I’m not sure how to make this work within a LigGDX project (I have not been able to find any LibGDX tutorials on this). Is this what I need to do and would this go in the AndroidLauncher.java or does it need a separate file within the Android project?
Code: https://codinginflow.com/tutorials/android/run-time-permission-request
Video Tutorial: https://www.youtube.com/watch?v=SMrB97JuIoM
Here is my simple LibGDX example which works if I target a lower API (20) but it crashes when I change it to API 26 because I’m not handling the runtime user permissions. In my example, clicking the button labeled “PRESS” causes a String to be saved to a text document via JSON. Then the string is read back out of the text file into a label and displayed on screen. The counter increases each time the button is clicked to show that a new save and load event has occurred.
If someone could help get my sample program working with API 26 (or later) or provide a different working example in LibGDX that I could download, run, and examine I would greatly appreciate it. Thanks.
Test.java
public class Test implements ApplicationListener {
private Stage stage;
private Window window;
private Graphics graphics;
@Override
public void create () {
stage = new Stage();
stage.setViewport(new ScreenViewport(stage.getViewport().getCamera()));
Gdx.input.setInputProcessor(stage);
graphics = new Graphics();
window = new Window(graphics);
stage.addActor(window);
}
@Override
public void render () {
Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
stage.act(Gdx.graphics.getRawDeltaTime());
stage.draw();
}
@Override
public void resize(int width, int height) {
stage.getViewport().update(width, height, true);
}
@Override
public void pause() { }
@Override
public void resume() { }
@Override
public void dispose () {
stage.dispose();
graphics.dispose();
}
}
Window.java
public class Window extends Table {
Graphics graphics;
FileHandle handle;
Label label;
int count=0;
public Window(Graphics graphics){
setFillParent(true);
this.graphics = graphics;
handle = Gdx.files.getFileHandle("ZTEST/test.txt", getFileType());
// label
Label.LabelStyle labelStyle = new Label.LabelStyle();
labelStyle.font = graphics.getFont();
labelStyle.fontColor = Color.WHITE;
label = new Label("", labelStyle);
label.setAlignment(Align.center);
add(label).size(graphics.programWidth*0.4f,graphics.programWidth*0.14f);
label.debug();
row().pad(graphics.programWidth*0.1f);
// button
TextButton.TextButtonStyle buttonStyle = new TextButton.TextButtonStyle();
buttonStyle.font = graphics.getFont();
buttonStyle.fontColor = Color.WHITE;
final TextButton button = new TextButton("PRESS", buttonStyle);
button.addListener(new ClickListener(){
@Override
public void clicked(InputEvent event, float x, float y){
// SAVE TO DISK
save(++count);
// LOAD FROM DISK
load();
}
});
add(button).size(graphics.programWidth*0.4f,graphics.programWidth*0.14f);
button.debug();
}
private Files.FileType getFileType(){
switch(Gdx.app.getType()){
case Desktop:
return Files.FileType.Local;
default:
return Files.FileType.External;
}
}
private void save(int count){
Json json = new Json();
json.setOutputType(JsonWriter.OutputType.json);
handle.writeString(json.prettyPrint("save: "+count), false);
}
private void load(){
if(handle.exists()){
final Json json = new Json();
label.setText(json.fromJson(null, handle.readString()).toString());
}
}
}
Graphics.java
(Creates the BitmapFont)
public class Graphics implements Disposable {
public final int programWidth;
BitmapFont font;
public Graphics(){
programWidth = (Gdx.graphics.getWidth() < Gdx.graphics.getHeight()) ? Gdx.graphics.getWidth() : Gdx.graphics.getHeight();
font = generateNumberFont();
}
private BitmapFont generateNumberFont(){
final FreeTypeFontGenerator generator = new FreeTypeFontGenerator(Gdx.files.internal("internal/arialbd.ttf"));
final FreeTypeFontGenerator.FreeTypeFontParameter parameter = new FreeTypeFontGenerator.FreeTypeFontParameter();
parameter.characters = "abcdefthijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890:";
parameter.size = (int)(programWidth*0.08f);
final BitmapFont font = generator.generateFont(parameter);
generator.dispose();
return font;
}
public BitmapFont getFont(){ return font; }
@Override
public void dispose() {
font.dispose();
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tekker.test" >
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/GdxTheme" >
<activity
android:name="com.tekker.test.AndroidLauncher"
android:label="@string/app_name"
android:screenOrientation="fullUser"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>