Categories
Status

Charts for Flutter and Flutter Web

Many apps have a need to display some sort of data in chart form. Recently, I was tasked with implementing a chart solution on Flutter and Flutter Web and wanted to walk you through the implementation.

In this example, we are going to use the charts_flutter package.

To get started we need to add it as a dependency in our pubspec.yaml

dependencies:
  charts_flutter: ^0.8.1

This package has a great example gallery that includes code snippets that show how to implement each example. In this post we are going to walk through a time series chart implementation.

The end result will be an app that displays a chart on Flutter Mobile and Web.

Once we have imported our dependency, we need to get the data from our data source and transform it into a model that the chart library knows how to use. To do this, we are going to start with the repository pattern. We will create a ReportRepository class and fetch our reports from it. The data we are using comes from a sensor device and the sensor devices provides us with vibration and trip count data.

Each report entry will look like this:

 {
    "date": "2020-01-15T19:00:00-0500",
    "trips": 553,
    "vibration": 1.08154506437768,
    "doors": 1161
  },

We will parse the data from a local json file and then return it as a list of reports.

https://github.com/dazza5000/flutter_chart_example/blob/master/lib/report.dart

class Report {
  final String date;
  final double vibration;

  Report({this.date, this.vibration});

  factory Report.fromJson(Map<String, dynamic> json) {
    return Report(
        date: json['date'] as String, vibration: json['vibration'] as double);
  }
}

For our example, we are only interested in the vibration data so that is all we will parse into the report object.

Next, we will convert our “API” data into model objects that the charts_flutter can use. charts_flutter expects a series of data associated with a domain and they provide a factory for populating the object they can use. An excerpt can be seen below.

https://github.com/google/charts/blob/master/charts_common/lib/src/data/series.dart#L108-L112

 factory Series(
      {@required String id,
      @required List<T> data,
      @required TypedAccessorFn<T, D> domainFn,
      @required TypedAccessorFn<T, num> measureFn,

With this, we know we need to take our Report entries which include a date and a measurement for vibration and populate the Series factory that we can see above.

We will start by converting our report data yet again into something that we can use with the Series factory. Note: We could have done this when we got the data from the API, but sometimes it is better to keep the model for the rest of your domain pure and map it to the model that is currently being used when necessary. This helps create a separation and minimize impact should other areas of your application begin to rely on the report model. This is the model we will be mapping our entries to:

class VibrationData {
  final DateTime time;
  final double vibrationReading;

  VibrationData(this.time, this.vibrationReading);
}

Next, we will utilize the Series factory that we talked about earlier. For this, we have created a static function that takes in a list of VibrationData and returns a Series type that we can use with charts_flutter

  static List<Series> _createChartData(List<VibrationData> vibrationData) {

    var data = [
      new Series<VibrationData, DateTime>(
        id: 'Desktop',
        colorFn: (_, __) => MaterialPalette.green.shadeDefault,
        domainFn: (VibrationData vibrationData, _) => vibrationData.time,
        measureFn: (VibrationData vibrationData, _) => vibrationData.vibrationReading,
        data: vibrationData,
      ),
    ];

    return data;
  }
}

Source: https://github.com/dazza5000/flutter_chart_example/blob/master/lib/chart_util.dart#L30

Once we’ve done that, we are ready to use the Series data with our chart! We’re almost there. All it has to have is our Series data

  TimeSeriesChart(
    List<common.Series<dynamic, DateTime>> seriesList, {etc})

So with that, we plop in the Seriesdata we created earlier and the chart will be rendered!

TimeSeriesChart(vibrationData)

Our sample implementation, sets some of the optional fields on the TimeSeriesChart and wraps it in a FractionallySizedBox

FractionallySizedBox(
                    child: TimeSeriesChart(
                      vibrationData,
                      defaultRenderer: new LineRendererConfig(
                          includeArea: true, stacked: true),
                      animate: false,
                      domainAxis:
                          new DateTimeAxisSpec(renderSpec: NoneRenderSpec()),
                    )

https://github.com/dazza5000/flutter_chart_example/blob/master/lib/main.dart

And we’re done! This will be rendered successfully on both mobile and web! 🙂

The full sample project is below. Thank you!

https://github.com/dazza5000/flutter_chart_example

Categories
Status

Google Maps for Flutter Web

This post will outline a Google Maps solution for Flutter Web.

TLDR: https://stackoverflow.com/a/57746754/3268303

The end result will have a google map centered on Texas with one marker and look like this:

To get started we need a google maps dependency. Open up pubspec.yaml and add the following:

google_maps: ">=3.0.0 <4.0.0"

Example: https://github.com/dazza5000/flutter_web_google_maps_example/blob/master/pubspec.yaml

Now we need to add our API Key for google maps to our index.html file in our web directory:

  <script src="https://maps.googleapis.com/maps/api/js?key=YOURAPIKEYHERE"></script>

Example: https://github.com/dazza5000/flutter_web_google_maps_example/blob/master/web/index.html

We are going to use dart inside an HtmlElementView which will return a widget that we can attach to our Flutter Web widget hierarchy. Without further ado, lets look at what that looks like in code.

Widget getMap() {
    String htmlId = "7";

    // ignore: undefined_prefixed_name
    ui.platformViewRegistry.registerViewFactory(htmlId, (int viewId) {

      final myLatlng = new LatLng(30.2669444, -97.7427778);

      final mapOptions = new MapOptions()
        ..zoom = 8
        ..center = new LatLng(30.2669444, -97.7427778);

      final elem = DivElement()
        ..id = htmlId
        ..style.width = "100%"
        ..style.height = "100%"
        ..style.border = 'none';

      final map = new GMap(elem, mapOptions);

      Marker(MarkerOptions()
        ..position = myLatlng
        ..map = map
        ..title = 'Hello World!');

      return elem;
    });

    return HtmlElementView(viewType: htmlId);
  }

In the code above we use Dart to interact with the Google Map library we added to our project. The library wraps the the javascript API with dart and with that we get Dart IDE support and type safety. Looking at the block, we can see that we specify a LatLng object and set that to our map center. Lastly, we create a single marker and add it to the map.

The full source code for this example can be found here: https://github.com/dazza5000/flutter_web_google_maps_example

A youtube walkthrough video is here too: https://www.youtube.com/watch?v=iW7pCBL7yWk&feature=youtu.be

Categories
Status

How to programmatically set your app as the default app for a USB device on Android

If you are looking to set your app as the default app for a UsbDevice and do not need to do it programmatically, then I recommend you follow the answers outlined on this SO question. I have a project on GitHub that provides a sample implementation of one of the answers. If you need to set this permission programmatically, then keep reading…

The problem

Initially, we implemented the manual solution outlined in the SO question above on a kiosk type device, but it was still a challenge for our operations team to remember to check the appropriate box before leaving the customer site. This would leave our kiosk device in a state where it was unable to talk to a USB card reader attached to it. We can modify our staging process to do this manual operation, but we also have 100’s of tablets in the field already deployed that need remediation. Our platform runs on a modified tablet that we have root access on. I started to dig through the framework code to see how things work and to see if there was something we could do programmatically.

Some background

When we interact with UsbDevice‘s on Android, we typically do that through the UsbManager service. This is an Android system service that has an implementation exposed to App developers through the Android SDK and a platform implementation that is implemented in the Android Platform source. The SDK implementation (UsbManager) talks to the UsbService using AIDL. When we get the UsbManager service, the methods we call on it call the the IUsbManager interface methods that are implemented by the UsbService on the platform side.

We can see the UsbService getting started by the SystemServer platform code here

                if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_USB_HOST)
                        || mPackageManager.hasSystemFeature(
                                PackageManager.FEATURE_USB_ACCESSORY)) {
                    // Manage USB host and device support
                    Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "StartUsbService");
                    mSystemServiceManager.startService(USB_SERVICE_CLASS);
                    Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                }

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/java/com/android/server/SystemServer.java#L988-L994

That block will eventually end up instantiating the UsbService on the platform side here:

        @Override
        public void onStart() {
            mUsbService = new UsbService(getContext());
            publishBinderService(Context.USB_SERVICE, mUsbService);
        }

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbService.java#L65

Don’t lose patience, we are almost to something interesting. The UsbService class handles the management of UsbDevices, but it delegates settings management to a UsbSettingsManager class. The UsbSettingsManager class references a settings file

        mSettingsFile = new AtomicFile(new File(
                Environment.getUserSystemDirectory(user.getIdentifier()),
                "usb_device_manager.xml"));

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbSettingsManager.java#L512

The settings file is read when the UsbSettingsManager is instantiated and the entries inside the file, called DeviceFilter entries, are added to an in memory map:

    // Maps DeviceFilter to user preferred application package
    private final HashMap<DeviceFilter, String> mDevicePreferenceMap =
            new HashMap<DeviceFilter, String>();

https://github.com/aosp-mirror/platform_frameworks_base/blob/eb72a7f4579b61031b4cb66afa43a82f823430f5/services/usb/java/com/android/server/usb/UsbSettingsManager.java#L89

A sample usb_device_manager.xml file with on DeviceFilter

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<settings>
    <preference package="fs.instore">
        <usb-device vendor-id="2049" product-id="25" class="0" subclass="0" protocol="0" manufacturer-name="MagTek" product-name="USB Swipe Reader" serial-number="B48B67B" />
    </preference>
</settings> 

This is the file that is created when a user connects a UsbDevice, the framework detects that an app has an intent filter to look for this device, and the user selects “Use by default for this USB device” for the particular device. In short, this is the file we need to programmatically create.

Sample dialog:

The usb_device_manager.xml file is located at /data/system/users/0/usb_device_manager.xml on our device.

Lastly, we need to know that this file is only read at USBService start and the in memory map is not exposed publicly. This means that any modifications to this file will require a reboot or service stop/start before they can be used.

The solution

  1. Write the usb_device_manager.xml file with details for our app/device.
  2. Restart the UsbService so that it will read the modified file and use the new values.

Writing usb_device_manager.xml

We can use some of the publicly available platform code to help us write the usb_device_manager.xml file. The framework uses an XML Serializer to write to the settings file

https://github.com/aosp-mirror/platform_frameworks_base/blob/nougat-mr2.3-release/core/java/com/android/internal/util/FastXmlSerializer.java

We can borrow this to write the file or we can use the serializer built into the SDK.

We also need to know the model we need to write. Earlier I mentioned the entries in the XML file were DeviceFilter entries. The DeviceFilter class is a private static inner class of UsbSettingsManager. We can “borrow” this code and the DeviceFilter constructor that takes a UsbDevice and use it in our solution.

                    DeviceFilter deviceFilter = new DeviceFilter(usbDevice);
                    writeSettingsFile(deviceFilter);

Now that we can recreate the same XML as the framework, we need to write to usb_device_manager.xml file. We can modify some of the platform code for our use.


    public static void writeSettingsLocked(Context context, DeviceFilter filter) {

        AtomicFile mSettingsFile = new AtomicFile(new File(context.getExternalFilesDir(null), "usb_device_manager.xml"));

        FileOutputStream fos = null;
        try {
            fos = mSettingsFile.startWrite();

            XmlSerializer serializer = new FastXmlSerializer();
            serializer.setOutput(fos, StandardCharsets.UTF_8.name());
            serializer.startDocument(null, true);
            serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            serializer.startTag(null, "settings");

                serializer.startTag(null, "preference");
                serializer.attribute(null, "package", "fs.instore");
                filter.write(serializer);
                serializer.endTag(null, "preference");


            serializer.endTag(null, "settings");
            serializer.endDocument();

            mSettingsFile.finishWrite(fos);
        } catch (IOException e) {
            Log.e(TAG, "Failed to write settings", e);
            if (fos != null) {
                mSettingsFile.failWrite(fos);
            }
        }
    }

And once we are done writing the file we need to copy it to the correct location and make sure its file permissions are correct. The following commands are executing programmatically in a shell environment on the kiosk device.

    public static final String COMMAND_COPY_USB_FILE = "cp /sdcard/Android/data/com.whereisdarran.setusbdefault/files/usb_device_manager.xml /data/system/users/0/usb_device_manager.xml";
    public static final String COMMAND_CHOWN_USB_FILE = "chown system:system /data/system/users/0/usb_device_manager.xml";

Lastly, we need to restart the UsbService.

We can do that by simply issuing a reboot command. Alternatively, in a root shell we could issue the stop command followed by a start command to bounce all of the system services.

The full solution.

Thank you for reading!

Categories
Status

How to setup boot on charge on TB-8504X/F running Nougat

Note: The Oreo ROM for this device supports fastboot oem off-mode-charge 0 This tutorial is for the Nougat ROM.

I was able to get it to work by updating the init.rc file

I found the on charger trigger and added the following lines below it:

setprop ro.bootmode "normal"
setprop sys.powerctl "reboot"

The entire trigger block ends up looking like this

on charger
    class_start charger
    setprop ro.bootmode "normal"
    setprop sys.powerctl "reboot"

You then need to repack and flash the boot image created after the updates.

Connect the device over USB

Power on device and get to bootloader mode

adb reboot bootloader 

To flash boot image execute the following command while in fastboot

fastboot flash boot new-boot.img

Note: This fix will cause the device to reboot when its plugged in even when shutting it off using the power button or software shutdown.

Source: https://forum.xda-developers.com/showthread.php?p=77766638#post77766638

You can see the commit that contains these changes for my project here:

https://github.com/darran-kelinske-fivestars/mkbootimg_tools/commit/c8633a2ec2e1924272fd16a8ed7fa7414bb65544#diff-d0007d763b44198f7c22e2c19609d5bbR606

Categories
Status

How to build TWRP for the TB-8504X/F

I recently started a project to build LineageOS for the Lenovo TB-8504X/F. Part of the journey typically involves building a custom recovery. TWRP is now the defacto so I started there.

Prerequisites:

  1. An Android build environment: https://source.android.com/setup/build/initializing or something like https://github.com/jfloff/docker-lineageos
  2. Repo Installed: https://source.android.com/setup/build/downloading

Steps:

  1. Create a working directory and cd into it
    • mkdir twrp && cd twrp
  2. Check out limited manifest for building TWRP
    • repo init -u git://github.com/minimal-manifest-twrp/platform_manifest_twrp_omni.git -b twrp-7.1
  3. Sync
    • repo sync
  4. Make a directory for the tb_8504x device inside the repository directory and cd into it
    • mkdir -p device/lenovo/tb_8504x && cd device/lenovo/tb_8504x
  5. Clone the recovery source into the tb_85004x directory
    • git clone git@github.com:Matshias/twrp_android_device_tb_8504x.git
  6. Go back to the top level directory and lets start to build
    • source build/envsetup.sh
      lunch omni_tb_8504x-userdebug
      mka recoveryimage

If everything goes well you should have a recovery image available in out/target/product/tb_8504x/

Big thanks to https://github.com/Matshias

You can find the final output and a continuing thread here: https://forum.xda-developers.com/thinkpad-tablet/general/twrp-root-tab-4-plus-tb-x704l-f-tb-t3664407

Categories
Status

Tips and Tricks with Flutter HTTPClient

On a recent project, I had the opportunity to use the HTTPClient to integrate with a medical device. During the integration, I learned a few things that are not immediately apparent from the docs and wanted to share what I learned with you all.

Sometimes you need to use Charles Proxy to debug what is happening. The HTTPClient ignores proxies by default. We can modify the HTTPClient to use the proxy with the following configuration:

Using a Proxy (Charles Proxy/Fiddler)

HttpClient client = HttpClient();
client.findProxy = (uri) {
  return "PROXY 192.168.1.77:3128;";
};

A more advanced configuration that will override a bad certificate check:

  static HttpClient getHttpClient() {
    HttpClient client = new HttpClient()
      ..findProxy = (uri) {
        return "PROXY 192.168.1.199:8888;";
      }
      ..badCertificateCallback =
      ((X509Certificate cert, String host, int port) => true);

    return client;
  }

Configuring Digest Access Authentication

Basic Authentication is fairly straightforward to integrate, but Digest Access Authentication takes a little more work. Below you can see a sample Digest Access Authentication configuration:

static HttpClient getHttpClient(String userName, String password) {
HttpClient client = new HttpClient()
..badCertificateCallback =
((X509Certificate cert, String host, int port) => true)
client.authenticate = (uri, scheme, realm) {
client.addCredentials(
uri, realm, new HttpClientDigestCredentials(userName, password));
return new Future.value(true);
};
return client;
}

The above snippet sets a function on the authenticate property of the client. When a resource the client is connecting to asks for authentication, we will supply HttpClientDigestCredentials to authenticate the request. I found this to work well with GET requests, but would fail with POST requests that included an attachment. This is because the client retries the request once credentials are provided, but it does not include attachments when replaying POST requests

Digest Authentication with POST

To handle the situation with Digest Authentication not working with POST, I created an authentication header that I would send with the initial POST request:

String ha1 = getHA1(userName, "websvc", password);
String ha2 = getHA2("POST", "/1.0/restrictedResource");
String response = getResponse(ha1, ha2, "3a15147c7e97b692f01a78ad53f887cd", "00000001", "SpL0Prz6");

HttpClientRequest httpClientRequest = await client.postUrl(Uri.parse(path));
httpClientRequest.headers.contentLength = csvAttachment.length;
httpClientRequest.headers.contentType = ContentType.text;
httpClientRequest.headers.add("Authorization", 'Digest username="$userName", realm="websvc", nonce="3a15147c7e97b692f01a78ad53f887cd", uri="/1.0/restrictedResource", algorithm="MD5", qop=auth, nc=00000001, cnonce="SpL0Prz6", response="$response"');

httpClientRequest.write(csvAttachment);

return await httpClientRequest.close();

// Helper methods

static String getHA1(String username, String realm, String password) {
List<int> toConvert = [];
toConvert.addAll(utf8.encode(username));
toConvert.add(58);
toConvert.addAll(realm.codeUnits);
toConvert.add(58);
toConvert.addAll(utf8.encode(password));
return md5.convert(toConvert).toString();
}

static String getHA2(String method, String url) {
List<int> toConvert = [];
toConvert.addAll(utf8.encode(method));
toConvert.add(58);
toConvert.addAll(utf8.encode(url));
return md5.convert(toConvert).toString();
}

static String getResponse(String ha1, String ha2, String nonce, String nonceCount, String cnonce) {
List<int> toConvert = [];
toConvert.addAll(utf8.encode(ha1));
toConvert.add(58);
toConvert.addAll(utf8.encode(nonce));
toConvert.add(58);
toConvert.addAll(utf8.encode(nonceCount));
toConvert.add(58);
toConvert.addAll(utf8.encode(cnonce));
toConvert.add(58);
toConvert.addAll(utf8.encode("auth"));
toConvert.add(58);
toConvert.addAll(utf8.encode(ha2));
return md5.convert(toConvert).toString();
}

Those are a few tips/tricks I picked up. Maybe they will help you along your journey. Thank you!

Categories
Status

Friendly Austin is now over 1200 Members!

Friendly Austin was started almost a year ago and we recently hit 1200 members.

Some of the events I’ve particularly enjoyed are Movies in the Park, Hiking in the Greenbelt, and dancing and listening to swing and Motown music.

The Meetup group has sometimes been a challenge, but I am thankful for all of the experiences that we’ve shared together.

I have posted a few events on It’s Time Texas and am excited to see what ITT has planned for the rest of 2016.

You can join Friendly Austin at this link!

Thank you!

Categories
How To

Getting started with Android and CircleCI

Continuous Integration provides many benefits to the software development process. Did I break the build? How many of our tests pass on the latest version? How can we distribute the latest version of our app to our testers smoothly? Continuous Integration can help answer all of these questions.

For those that have been around for a while, there have been many CI tools (CruiseControl) along the road to CircleCI and many are still relevant (Jenkins).

Yesterday, I started looking at CircleCI in depth after watching Donn Felker’s excellent video on Caster.io and here are a few simple steps to get started with CircleCI and Android.

To start, sign up for a CircleCI account here and sync your Github repositories.

Add a project that you would like to add to CircleCI. CircleCI will try to build your project automatically, but to maintain better control over your CI process you should use a circle.yml file. This file should be placed in the base directory of your project.

A sample circle.yml can be found below. Change the words CriminalIntent to the name of your project and make sure that the build-tools version in the circle.yml matches the build tools version in your build.gradle file for your app module.

https://gist.github.com/dazza5000/0a807de012c7281f14787cf30bf7be0e

Check in the circle.yml file and push it to GitHub.

CircleCI should now automatically build your project!

Screen Shot 2016-05-01 at 6.23.56 PMTo see what a passing build looks like you can check here:

https://circleci.com/gh/dazza5000/CriminalIntent

Full source for the project including the circle.yml file and build.gradle can be found here:

https://github.com/dazza5000/CriminalIntent

 

giphy

Categories
How To

Where to put the gradle.properties file on Jenkins

TL;DR: Put your gradle.properties file in the /var/lib/jenkins/.gradle/ directory on a standard Ubuntu install.

Sometimes you need to have a gradle.properties file that is not included in your git repository. Many times this is because you need to have an api key that you would not like to be made public.

Because your gradle.properties file will not be checked into your git repository, the job will fail to build if your api key value is not able to be found.

One way to fix this is by placing your gradle.properties file in a place that jenkins will look for it. By default this location is in the .gradle directory of your Jenkins user.home directory.

To find the user.home directory open the Jenkins console in your browser, click “Manage Jenkins”, and then click the “System Information” link.

Scroll down until you find “user.home”

Screenshot_2016-02-08_17-26-12

In this example, the user.home directory is /var/lib/jenkins.

Now that we know this, we need to copy our gradle.properties file that contains our api key to /var/lib/jenkins/.gradle

For example:

sudo cp ~/source/a-caring-text/gradle.properties /var/lib/jenkins/.gradle/

Change the file permissions so that Jenkins can read the file:

sudo chown jenkins:jenkins /var/lib/jenkins/.gradle/gradle.properties

Now you can build the job that was depending on this api key!

raw

Categories
Status

Friendly Austin hit 900 Members :)

We’re about 8 months in and this week we hit 900 members.

We are reaching a critical mass of 1000. Now we’re working on planning the rest of 2016 and enhancing our social media on Facebook and Twitter.

Lastly, Friendly Austin is now working with It’s Time Texas and is excited about what they have planned for 2016.

Thank you! <3

Click this link to join Friendly Austin!