New Gluon Charm Down Services (Part I)


The Gluon engineering team has over the past few weeks been working on enhancing the already crowded list of services in Charm Down with a few more plugins. These services will be released in the coming days (or, depending on when you read this, might already be available).

In this blog post, we’ll talk about two of these: Video and Share services, and new common functionality included in all the services: runtime permissions check on Android and a debug flag.

You can find the latest code in the Charm Down repository.

Charm Down

Charm Down is the library that addresses the integration with low-level platform APIs. Using Charm Down, you write code that accesses device and hardware features using a uniform, platform-independent API. At runtime, the appropriate implementation (desktop, android, ios, etc) transparently makes sure the platform specific code is used to deliver the functionality.

Gluon Charm Down is another Gluon open source project, available in our gluon-oss repo. It is also bundled as part of our Gluon Mobile offering, for building high-quality, cross-device applications for Android and iOS. Developers interested in understanding the API can review the Charm Down documentation or the full Charm Down Javadoc.

Video Service

With the video service you can play media files (both video and audio) on your device. The media files can be provided from the resources folders or from valid URLs.

The video service displays videos on top of the UI layer. This means that the developer should take care of disabling view switching and any other UI interaction that could lead to overlapping nodes while the video is playing. If the user calls hide() or the playlist finishes, the media layer will be removed, and the user will be able to resume normal interaction.

Video files can be displayed with or without native embedded controls, and its maximum size and position on the screen can be defined.

The following code snippet shows how you can display video in your mobile app, providing the file BigBuckBunny_320x180.mp4 (downloaded from here, licensed under the Creative Commons Attribution 3.0 license) is added to the assets folder:

Services.get(VideoService.class).ifPresent(video -> {
    video.setPosition(Pos.CENTER, 0, 40, 0, 40);
    video.setControlsVisible(true);
    video.getPlaylist().add("BigBuckBunny_320x180.mp4");
    video.show();
});

Once the media player is visible, you can use the embedded controls to play or fast forward the video file.

Full screen mode is enabled and can be accessed/exited with a pinch gesture. Also orientation changes are taken into account, so padding in normal screen mode has to be applied accordingly.

The following is all the code required to create an app that can play video files from the local folder (place the file “BigBuckBunny_320x180.mp4” under /src/main/resources/) and from the Internet, using the Gluon IDE plugin and the Single View project template, providing buttons to interact with the playlist in this case.

// build.gradle
jfxmobile {
    downConfig {
        version = '3.6.0'
        plugins 'display', 'lifecycle', 'statusbar', 'storage', 'video'
    }
}
public class BasicView extends View {

    private final List<String> playlist = Arrays.asList("BigBuckBunny_320x180.mp4", 
              "http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4");

    public BasicView(String name) {
        super(name);
        
        setOnShown(ev -> {
            System.setProperty("com.gluonhq.charm.down.debug", "true");
            Services.get(VideoService.class).ifPresent(video -> {
                video.setPosition(Pos.CENTER, 0, 40, 0, 40);
                video.setControlsVisible(false);
                video.getPlaylist().addAll(playlist);
                video.play();

                Button playButton = MaterialDesignIcon.PLAY_ARROW.button();
                playButton.setOnAction(e -> {
                    if (video.statusProperty().get() == Status.PLAYING) {
                        playButton.setGraphic(MaterialDesignIcon.PLAY_ARROW.graphic());
                        video.pause();
                    } else {
                        playButton.setGraphic(MaterialDesignIcon.PAUSE.graphic());
                        video.play();
                    }
                });
                final Button backButton = MaterialDesignIcon.CHEVRON_LEFT.button(e -> video.setCurrentIndex(video.currentIndexProperty().get() - 1));
                backButton.disableProperty().bind(video.currentIndexProperty().lessThan(1));
                
                final Button nextButton = MaterialDesignIcon.CHEVRON_RIGHT.button(e -> video.setCurrentIndex(video.currentIndexProperty().get() + 1));
                nextButton.disableProperty().bind(video.currentIndexProperty().greaterThan(playlist.size() - 2));
                
                final Button stopButton = MaterialDesignIcon.STOP.button(e -> video.stop());
                
                final Button fullScreenButton = MaterialDesignIcon.FULLSCREEN.button(e -> video.setFullScreen(true));
                fullScreenButton.setDisable(true);
                video.fullScreenProperty().addListener((obs, ov, nv) -> getApplication().getAppBar().setVisible(! nv));
                
                getApplication().getAppBar().getActionItems().addAll(backButton, playButton, nextButton, stopButton, fullScreenButton);
                
                video.statusProperty().addListener((obs, ov, nv) -> {
                    fullScreenButton.setDisable(nv != Status.PLAYING && nv != Status.PAUSED);
                    if (video.statusProperty().get() == Status.PLAYING) {
                        playButton.setGraphic(MaterialDesignIcon.PAUSE.graphic());
                    } else {
                        playButton.setGraphic(MaterialDesignIcon.PLAY_ARROW.graphic());
                    }    
                });
            });
        });
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
        appBar.setTitleText("Video Player");
    }
    
}

Share Service

The share service provides a way to share content (text and/or files) from the current mobile app by using other suitable apps existing on the user device.

This one-line snippet shows how you can easily share text from your app through the suitable apps, social media accounts, and other services installed in the user’s device.

Services.get(ShareService.class).ifPresent(service -> service.share("This is the subject", "This is the content of the message"));

To share files, it is convenient to make use of the Storage Service to create or read them in either private or public storage folders. Note that for sharing files on Android, those files have to be accessible from public storage folders.

The following is all the code required to create an app that can share text and files, using the Gluon IDE plugin and the Single View project template.

// build.gradle
jfxmobile {
    downConfig {
        version = '3.6.0'
        plugins 'display', 'lifecycle', 'statusbar', 'share', 'storage'
    }
}
// BasicView.java
public class BasicView extends View {

    public BasicView(String name) {
        super(name);
        
        Button buttonText = new Button("Share Text", MaterialDesignIcon.SHARE.graphic());
        Services.get(ShareService.class).ifPresent(s -> 
            buttonText.setOnAction(e -> s.share("Sharing a message", "Hi, this is sent using the Charm Down Share plugin.")));
        
        File root = Services.get(StorageService.class)
                .flatMap(s -> s.getPublicStorage("Documents"))
                .orElseThrow(() -> new RuntimeException("Folder not available")); 
        
        File file = new File(root, "icon.png");
        if (! file.exists()) {
            copyFromResources("/", root.getAbsolutePath(), "icon.png");
        }
        
        Button buttonFile = new Button("Share Image", MaterialDesignIcon.SHARE.graphic());
        Services.get(ShareService.class).ifPresent(s -> 
            buttonFile.setOnAction(e -> s.share("Sharing an icon", "Hi, I send you an image.", "image/png", file)));
        
        VBox controls = new VBox(15.0, buttonText, buttonFile);
        controls.setAlignment(Pos.CENTER);
        
        setCenter(controls);
    }

    @Override
    protected void updateAppBar(AppBar appBar) {
        appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
        appBar.setTitleText("Sharing");
    }
    
    private static void copyFromResources(String pathIni, String pathEnd, String name)  {

        try (InputStream myInput = BasicView.class.getResourceAsStream(pathIni+name)) {
            String outFileName =  pathEnd + "/" + name;
            try (OutputStream myOutput = new FileOutputStream(outFileName)) {
                byte[] buffer = new byte[1024];
                int length;
                while ((length = myInput.read(buffer)) > 0) {
                    myOutput.write(buffer, 0, length);
                }
                myOutput.flush();

            } catch (IOException ex) {
                System.out.println("Error " + ex);
            }
        } catch (IOException ex) {
            System.out.println("Error " + ex);
        }
    }
}

It doesn’t require any extra configuration, but on Android, since we are using the Storage service, we need to add the PermissionRequest activity to the manifest, as below, if we are targeting Android 23+:

<!-- AndroidManifext.xml -->
<manifest ...>
        ...
        <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="23"/>
        <application ...>
                <activity android:name="javafxports.android.FXActivity" ...>
                       ...
                <activity android:name="com.gluonhq.impl.charm.down.plugins.android.PermissionRequestActivity" />
        </application>
</manifest>

And on iOS, if we want to share the image to the local gallery, we need to add this key to the info file:

<!-- Default-Info.plist -->
<plist version="1.0">
<dict>
       ...
        <key>NSPhotoLibraryUsageDescription</key>
        <string>Required for image sharing</string>
</dict>
</plist>

Deploying and running the app on Android and iOS will let us share the text and image with our installed apps:

Runtime permissions request on Android

Since Android 6.0 (API level 23), users can grant permissions to apps while the app is running, not when they install the app, so including the required permissions in the AndroidManifest file is not enough. The user has more control over the app’s functionality as any or all the permissions can be granted or revoked at any time.

So far, the Charm Down plugins were not able to request permissions at runtime, and when targeting Android SDK 23+ the only way to grant dangerous permissions was via the app’s Settings screen.

As for the Android documentation, these permissions are those where the app wants data or resources that involve the user’s private information, or could potentially affect the user’s stored data or the operation of other apps. Examples of plugins that require these type of permissions are the DialerService (phone), the PicturesService (camera), the PositionService (location), and the StorageService (storage).

Since this release, all the plugins that require those type of permissions include an activity: the PermissionRequestActivity, that will manage these cases. All that is required is adding the activity to the Android manifest, when using barcode scan, dialer, pictures, position or the storage services.

For instance, in case you are targeting Android 23+, and you want to take a picture:

Services.get(PicturesService.class).ifPresent(service -> service.takePhoto(false));

by adding the required permission and the mentioned activity to the manifest:

<!-- AndroidManifext.xml -->
<manifest ...>
        ...
        <uses-permission android:name="android.permission.CAMERA"/>
        ... 
        <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="23"/>
        <application ...>
                <activity android:name="javafxports.android.FXActivity" ...>
                       ...
                <activity android:name="com.gluonhq.impl.charm.down.plugins.android.PermissionRequestActivity" />
        </application>
</manifest>

you will get a dialog the first time you run the app to grant permission or if later on the user revokes this permission in the app’s Settings screen.

Debug flag

Starting from this release, setting the system property "com.gluonhq.charm.down.debug" to true, right before initializing the service, will enable a more verbose console output.

System.setProperty("com.gluonhq.charm.down.debug", "true");
Services.get(VideoService.class).ifPresent(video -> video.play());

Conclusion

Two more services and common features have been added to Charm Down. Give them a try by running any of the samples available, or adding your own implementation.

Stay tuned, as in future posts we’ll talk about more services: bluetooth low energy for devices, audio recording, advertising, and in-app billing.

If you need extra features, new plugins or commercial support, you can get in touch with us at support@gluonhq.com.