Implementing pagination with Retrofit in Eventyay Attendee

Pagination (Paging) is a common and powerful technique in Android Development when making HTTP requests or fetching data from the database. Eventyay Attendee has found many situations where data binding comes in as a great solution for our network calls with Retrofit. Let’s take a look at this technique.

  • Problems without Pagination in Android Development
  • Implementing Pagination with Kotlin with Retrofit
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Making HTTP requests to fetch data from the API is a basic work in any kind of application. With the mobile application, network data usage management is an important factor that affects the loading performance of the app. Without paging, all of the data are fetched even though most of them are not displayed on the screen. Pagination is a technique to load all the data in pages of limited items, which is much more efficient

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1:  Set up dependency in build.gradle

// Paging
implementation "androidx.paging:paging-runtime:$paging_version"
implementation "androidx.paging:paging-rxjava2:$paging_version"

Step 2:  Set up retrofit to fetch events from the API

@GET("events?include=event-sub-topic,event-topic,event-type")
fun searchEventsPaged(
   @Query("sort") sort: String,
   @Query("filter") eventName: String,
   @Query("page[number]") page: Int,
   @Query("page[size]") pageSize: Int = 5
): Single<List<Event>>

Step 3: Set up the DataSource

DataSource is a base class for loading data in the paging library from Android. In Eventyay, we use PageKeyedDataSource. It will fetch the data based on the number of pages and items per page with our default parameters. With PageKeyedDataSource, three main functions loadInitial(), loadBefore(), loadAfter() are used to to load each chunks of data.

class EventsDataSource(
   private val eventService: EventService,
   private val compositeDisposable: CompositeDisposable,
   private val query: String?,
   private val mutableProgress: MutableLiveData<Boolean>

) : PageKeyedDataSource<Int, Event>() {
   override fun loadInitial(
       params: LoadInitialParams<Int>,
       callback: LoadInitialCallback<Int, Event>
   ) {
       createObservable(1, 2, callback, null)
   }

   override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
       val page = params.key
       createObservable(page, page + 1, null, callback)
   }

   override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Event>) {
       val page = params.key
       createObservable(page, page - 1, null, callback)
   }

   private fun createObservable(
       requestedPage: Int,
       adjacentPage: Int,
       initialCallback: LoadInitialCallback<Int, Event>?,
       callback: LoadCallback<Int, Event>?
   ) {
       compositeDisposable +=
           eventService.getEventsByLocationPaged(query, requestedPage)
               .withDefaultSchedulers()
               .subscribe({ response ->
                   if (response.isEmpty()) mutableProgress.value = false
                   initialCallback?.onResult(response, null, adjacentPage)
                   callback?.onResult(response, adjacentPage)
               }, { error ->
                   Timber.e(error, "Fail on fetching page of events")
               }
           )
   }
}

Step 4: Set up the Data Source Factory

DataSourceFactory is the class responsible for creating DataSource object so that we can create PagedList (A type of List used for paging) for events.

class EventsDataSourceFactory(
   private val compositeDisposable: CompositeDisposable,
   private val eventService: EventService,
   private val query: String?,
   private val mutableProgress: MutableLiveData<Boolean>
) : DataSource.Factory<Int, Event>() {
   override fun create(): DataSource<Int, Event> {
       return EventsDataSource(eventService, compositeDisposable, query, mutableProgress)
   }
}

Step 5: Adapt the current change to the ViewModel. 

Previously, events fetched in List<Event> Object are now should be turned into PagedList<Event>.

sourceFactory = EventsDataSourceFactory(
   compositeDisposable,
   eventService,
   mutableSavedLocation.value,
   mutableProgress
)
val eventPagedList = RxPagedListBuilder(sourceFactory, config)
   .setFetchScheduler(Schedulers.io())
   .buildObservable()
   .cache()

compositeDisposable += eventPagedList
   .subscribeOn(Schedulers.io())
   .observeOn(AndroidSchedulers.mainThread())
   .distinctUntilChanged()
   .doOnSubscribe {
       mutableProgress.value = true
   }.subscribe({
       val currentPagedEvents = mutablePagedEvents.value
       if (currentPagedEvents == null) {
           mutablePagedEvents.value = it
       } else {
           currentPagedEvents.addAll(it)
           mutablePagedEvents.value = currentPagedEvents
       }
   }, {
       Timber.e(it, "Error fetching events")
       mutableMessage.value = resource.getString(R.string.error_fetching_events_message)
   })

Step 6: Turn ListAdapter into PagedListAdapter

PageListAdapter is basically the same ListAdapter to update the UI of the events item but specifically used for Pagination. In here, List objects can also be null.

class EventsListAdapter : PagedListAdapter<Event, EventViewHolder>(EventsDiffCallback()) {

   var onEventClick: EventClickListener? = null
   var onFavFabClick: FavoriteFabClickListener? = null
   var onHashtagClick: EventHashTagClickListener? = null

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
       val binding = ItemCardEventsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
       return EventViewHolder(binding)
   }

   override fun onBindViewHolder(holder: EventViewHolder, position: Int) {
       val event = getItem(position)
       if (event != null)
           holder.apply {
               bind(event, position)
               eventClickListener = onEventClick
               favFabClickListener = onFavFabClick
               hashTagClickListAdapter = onHashtagClick
           }
   }

   /**
    * The function to call when the adapter has to be cleared of items
    */
   fun clear() {
       this.submitList(null)
   }

AND HERE ARE THE RESULTS…

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data-binding is that sometimes, it is pretty hard to debug with unhelpful log messages. Hopefully, you can empower your UI in your project now with data-binding. 

Pagination is the way to go for fetching items from the API and making infinite scrolling. This helps reduce network usage and improve the performance of Android applications. And that’s it. I hope you can make your application more powerful with pagination. 

RESOURCES

Open Event Codebase: https://github.com/fossasia/open-event-attendee-android/pull/2012

Documentation: https://developer.android.com/topic/libraries/architecture/paging/ 

Google Codelab: https://codelabs.developers.google.com/codelabs/android-paging/#0

Continue Reading Implementing pagination with Retrofit in Eventyay Attendee

Importing files from local storage in PSLab Android application

This blog demonstrates how a user can import log files from local storage to the PSLab Android application for various instruments and play them. This functionality is really useful as users can share their log files and import them in their app. This blog mostly consists of my work in the PSLab Android repository.

How to access local storage files?

We here use the concept of implicit intent to access the local storage of the device and then generate the file from the received data URI.

Implicit intents differ from explicit intents in a way that, they don’t give exact class or activity to be initialized through the intent, instead they provide the action to be performed and the class or activities are selected implicitly from the required action

The code block is shown below. 

private void selectFile() {
        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        startActivityForResult(intent, 100);
}

Here the Intent.ACTION_GET_CONTENT defines implicit intent. This intent opens the activity related to the action of GETTING CONTENT. The type of content is specified in the Intent.setType(<TYPE>). Since here the type is set to “*/*”, it will open all types of files. If we want only images we can set Type to “images”.

startActivityForResult(intent, <REQUEST_CODE>) starts the file selection activity. 

How to generate a file from received URI?

Once the user selects a file from the file selection activity we can generate the selected file from the data passed in the callback function of startActivityForResult(). The data intent passed as a parameter to onActivityResult() callback contains data for the selected file. We can retrieve path, name, etc details of the selected file from this data intent. The code block for the same is given below.

@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (requestCode == 100) {
            if (resultCode == RESULT_OK) {
                Uri uri = data.getData();
                String path = uri.getPath();
                path = path.replace("/root_path/", "/");
                File file = new File(path);
                getFileData(file);
            }
            else Toast.makeText(this, this.getResources().getString(R.string.no_file_selected), Toast.LENGTH_SHORT).show();
        }
    }

Here we check for the requestCode, which we passed when calling the startActivityForResult() function. We further check if the result is valid and then generate the file from the file path we received in the data Intent. Once we get the path we can get the selected file using the following lines of code:

String path = uri.getPath();
path = path.replace("/root_path/", "/");
File file = new File(path);

How to get Data from the file?

Once the file is generated, it is passed to a function getFileData(File file) to get data in the file to add to the logs of the selected device.  The main part of the getFileData function is given below.

FileInputStream is = new FileInputStream(file);
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line = reader.readLine();
int i = 0;
long block = 0, time = 0;
while (line != null) {
   if (i != 0) {
        String[] data = line.split(",");
        try {
              time += 1000;
              BaroData baroData = new BaroData(time, block, Float.valueOf(data[2]),                              Double.valueOf(data[3]), Double.valueOf(data[4]));
              realm.beginTransaction();
              realm.copyToRealm(baroData);
              realm.commitTransaction();
            } catch (Exception e) {
       Toast.makeText(this, getResources().getString(R.string.incorrect_import_format), Toast.LENGTH_SHORT).show();
           }
    }
    i++;
    line = reader.readLine();

Here we read the file line by line and convert the CSV data into the object of the selected device. And then this data is added to app storage using the realm. As shown in the code block above, we are parsing the data to the BarometerData class instances. We split each line by “,” and then use each field as input to the constructor of the BarometerData class. Once we create the instances of the class, we add them to the realm, so the imported file is saved in the realm and now we can access it easily from DataLoggerActivity.

The following images demonstrate the functionality of Import log 

Step 1: Select Import Log menu from 


(Figure 1: Import Log menu)

Step 2: Select the file to be imported from the local storage 


(Figure 2: Files to import from Local storage)

Step 3: Play the imported log from the DataLoggerActivity


(Figure 3: Imported logged data in DataLoggerActivity)

Resources

Tags: PSLab, Android, GSoC 19, ImportLog, Intents, Implicit Intent

Continue Reading Importing files from local storage in PSLab Android application

Adding time counter on ordering tickets in Eventyay Attendee

In Eventyay Attendee, ordering tickets for events has always been a core functionality that we focus on. When ordering tickets, adding a time counter to make a reservation and release tickets after timeout is a common way to help organizers control their tickets’ distribution and help users save up their tickets. Let’s take a look at how to implement this feature

  • Implementing the time counter 
  • Some notes on implementing time counter
  • Conclusion
  • Resources

INTEGRATING TIME COUNTER TO YOUR SYSTEM

Step 1: Create the UI for your time counter. In here, we made a simple View container with TextView inside to update the time.

Step 2: Set up the time counter with Android CountdownTimer with the total countdown time and the ticking time. In Eventyay, the default countdown time is 10 minutes (600,000 ms) with the ticking time is (1,000 ms), which means the UI is updated every one second.

private fun setupCountDownTimer(orderExpiryTime: Int) {
   rootView.timeoutCounterLayout.isVisible = true
   rootView.timeoutInfoTextView.text =
       getString(R.string.ticket_timeout_info_message, orderExpiryTime.toString())

   val timeLeft: Long = if (attendeeViewModel.timeout == -1L) orderExpiryTime * 60 * 1000L
                           else attendeeViewModel.timeout
   timer = object : CountDownTimer(timeLeft, 1000) {
       override fun onFinish() {
           findNavController(rootView).navigate(AttendeeFragmentDirections
               .actionAttendeeToTicketPop(safeArgs.eventId, safeArgs.currency, true))
       }

       override fun onTick(millisUntilFinished: Long) {
           attendeeViewModel.timeout = millisUntilFinished
           val minutes = millisUntilFinished / 1000 / 60
           val seconds = millisUntilFinished / 1000 % 60
           rootView.timeoutTextView.text = "$minutes:$seconds"
       }
   }
   timer.start()
}

Step 3: Set up creating a pending order when the timer starts counting so that users can hold a reservation for their tickets. A simple POST request about empty order to the API is made

fun initializeOrder(eventId: Long) {
   val emptyOrder = Order(id = getId(), status = ORDER_STATUS_INITIALIZING, event = EventId(eventId))

   compositeDisposable += orderService.placeOrder(emptyOrder)
       .withDefaultSchedulers()
       .subscribe({
           mutablePendingOrder.value = it
           orderIdentifier = it.identifier.toString()
       }, {
           Timber.e(it, "Fail on creating pending order")
       })
}

Step 4: Set up canceling order when the time counter finishes. As time goes down, the user should be redirected to the previous fragment and a pop-up dialog should show with a message about reservation time has finished. There is no need to send an HTTP request to cancel the pending order as it is automatically handled by the server.

Step 5: Cancel the time counter in case the user leaves the app unexpectedly or move to another fragment. If this step is not made, the CountdownTimer still keeps counting in the background and possibly call onFinished() at some point that could evoke functions and crash the app

override fun onDestroy() {
   super.onDestroy()
   if (this::timer.isInitialized)
       timer.cancel()
}

RESULTS

CONCLUSION

For a project with a ticketing system, adding a time counter for ordering is a really helpful feature to have. With the help of Android CountdownTimer, it is really to implement this function to enhance your user experience.

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1843 – Add time counter on ordering ticket

Documentation: https://developer.android.com/reference/android/os/CountDownTimer

Continue Reading Adding time counter on ordering tickets in Eventyay Attendee

Examples of how AsyncTask is used in PSLab Android App

In this blog, we will look at a very useful and important feature provided by Android – AsyncTask and more importantly how AsyncTasks have been put to use for various functionalities throughout the PSLab Android Project

What are Threads?

Threads are basically paths of sequential execution within a process. In a way, threads are lightweight processes. A process may contain more than one threads and all these threads are executed in parallel. Such a method is called “Multithreading”. Multithreading is very useful when some long tasks need to be executed in the background while other tasks continue to execute in the foreground.

Android has the main UI thread which works continuously and interacts with a user to display text, images, listen for click and touch, receive keyboard inputs and many more. This thread needs to run without any interruption to have a seamless user experience.

When AsyncTask comes into the picture?

AsyncTask enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.  

In PSLab Android application, we communicate with PSLab hardware through I/O(USB) interface. We connect the PSLab board with the mobile and request and wait for data such as voltage values and signal samples and once the data is received we display it as per requirements. Now clearly we can’t run this whole process on the main thread because it might take a long time to finish and because of that other UI tasks would be delayed which eventually degrade the user experience. So, to overcome this situation, we use AsyncTasks to handle communication with PSLab hardware.

Methods of AsyncTask  

AsyncTask is an Abstract class and must be subclassed to use. Following are the methods of the AsyncTask:

  • onPreExecute()
    • Used to set up the class before the actual execution
  • doInBackground(Params…)
    • This method must be overridden to use AsyncTask. This method contains the main part of the task to be executed. Like the network call etc.
    • The result from this method  is passed as a parameter to onPostExecute() method
  • onProgressUpdate(Progress…)
    • This method is used to display the progress of the AsyncTask
  • onPostExecute(Result)
    • Called when the task is finished and receives the results from the doInBackground() method

There are 3 generic types passed to the definition of the AsyncTask while inheriting. The three types in order are 

  1. Params: Used to pass some parameters to doInBackground(Params…) method of the Task 
  2. Progress: Defines the units in which the progress needs to be displayed/
  3. Result : Defines the data type to be returned from onInBackground() and receive as a parameter in the onPostExecute(Result) method

Example of the usage of the AsyncClass is as under : 

private class SampleTask extends AsyncTask<Params, Progress, Result> {
     @Override
     protected Result doInBackground(Params... params) {
          // The main code goes here
          return result;
     }
     @Override 
     protected void onProgressUpdate(Progress... progress) {
          // display the progress
     }
     @Override 
     protected void onPostExecute(Result result) {
         // display the result
     }
}

We can create an instance of this class as under and execute it.

SampleTask sampleTask = new SampleTask();
sampleTask.execute(params)

We can cancel a running class by calling the task.cancel() function

sampleTask.cancel()

AsyncTask in PSLab Android Application

As mentioned earlier some task which takes a lot of time, can’t be executed on the main thread. Hence in such cases AsyncTask is used. We will look into some examples where AsyncTask has been put to use in PSLab Android Application

Delete All Logs: 

In the DataLoggerActivity, user has an option to delete all the logs that have been saved on the local storage. Now there might be a lot number of log files that needs to be deleted. Hence it is better to use AsyncTask for these. The code snippet for this is below,

private class DeleteAllTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            Realm realm = Realm.getDefaultInstance();
            for (SensorDataBlock data : realm.where(SensorDataBlock.class)
                    .findAll()) {
                File logDirectory = new File(
                        Environment.getExternalStorageDirectory().getAbsolutePath() +
                                File.separator + CSVLogger.CSV_DIRECTORY +
                                File.separator + data.getSensorType() +
                                File.separator + CSVLogger.FILE_NAME_FORMAT.format(data.getBlock()) + ".csv");
                logDirectory.delete();
                realm.beginTransaction();
                realm.where(SensorDataBlock.class)
                        .equalTo("block", data.getBlock())
                        .findFirst().deleteFromRealm();
                realm.commitTransaction();
            }
            realm.close();
            return null;
        }
        @Override
        protected void onPostExecute(Void aVoid) {
            deleteAllProgressBar.setVisibility(View.GONE);
            if (LocalDataLog.with().getAllSensorBlocks().size() <= 0) {
                blankView.setVisibility(View.VISIBLE);
            }
        }
    }

As can be seen, we look for all the stored logs, and then delete each file one after another in doInBackground(). Once all the files are deleted, onPostExecute() is called, where we make the progress bar disappear. So, this how AsyncTask is used to implement deleteAllFiles feature.

Capture Task and Fourier Transform Output of Signals in Oscilloscope.

To display the generated signal in the oscilloscope, we call captureTraces() and fetchTraces functions from the ScienceLab class. Now, both these functions communicate with the PSLab Board, request for data, receives the data, manipulates it into the desired format and then display the signal on the Oscilloscope screen. Now clearly we can’t afford to run such a process on the main thread. So we use AsyncTask to handle it. 

In the Oscilloscope, there is a feature to see the fourier transform output of the signal generated by the oscilloscope. Now to generate the Fourier Transform Output of the signal, we use the Fast Fourier Transform method. The time complexity of  FFT (Fast Fourier Transform) is O(Nlog(N)), where N is the number of samples of the input signal. Now even if FFT is fast, we can risk to run this function on the main Thread. So once again we get help from AsyncTask.
Both of these functionalities are included in same AsyncTask Class called captureTask  A snippet for this task can be seen below,

public class CaptureTask extends AsyncTask<String, Void, Void> {
        private ArrayList<ArrayList<Entry>> entries = new ArrayList<>();
        private ArrayList<ArrayList<Entry>> curveFitEntries = new ArrayList<>();
        private Integer noOfChannels;
        private String[] paramsChannels;
        private String channel;
        @Override
        protected Void doInBackground(String... channels) {
            paramsChannels = channels;
            noOfChannels = channels.length;
            try {
                double[] xData;
                double[] yData;
                ArrayList<String[]> yDataString = new ArrayList<>();
                String[] xDataString = null;
                maxAmp = 0;
                for (int i = 0; i < noOfChannels; i++) {
                    entries.add(new ArrayList<>());
                    channel = channels[i];
                    HashMap<String, double[]> data;
                    if (triggerChannel.equals(channel))
                        scienceLab.configureTrigger(channelIndexMap.get(channel), channel, trigger, null, null);
                    scienceLab.captureTraces(1, samples, timeGap, channel, isTriggerSelected, null);
                    data = scienceLab.fetchTrace(1);

In this part of the capture Task class, we use the captureTrace() and fetchTrace() function to get the signal samples and then store them into the data variable. Below is the part where we use call the fft() for the input signal.

if (isFourierTransformSelected) {
     Complex[] yComplex = new Complex[yData.length];
     for (int j = 0; j < yData.length; j++) {
              yComplex[j] = Complex.valueOf(yData[j]);
     }
     fftOut = fft(yComplex);
}

This is a very simple part where we just call the Fast Fourier Transfer function is the user has selected to see the fourier transform output. The implementation of the Fourier function can be seen below,

 public Complex[] fft(Complex[] input) {
        Complex[] x = input;
        int n = x.length;
        if (n == 1) return new Complex[]{x[0]}; // if only single element, return as it is
        if (n % 2 != 0) {
            x = Arrays.copyOfRange(x, 0, x.length - 1);
        //No of samples should be even for this function to run, so i case of odd samples we remove the last element. This doesn’t affect the output significantly
        }
        Complex[] halfArray = new Complex[n / 2];
        for (int k = 0; k < n / 2; k++) {
            halfArray[k] = x[2 * k]; // Array of input terms at even places
        }
        Complex[] q = fft(halfArray); // recursive call for even terms
        for (int k = 0; k < n / 2; k++) {
            halfArray[k] = x[2 * k + 1]; // Array of terms at odd places
        }
        Complex[] r = fft(halfArray); // recursive call for odd terms
        Complex[] y = new Complex[n]; // Array of final output
        for (int k = 0; k < n / 2; k++) {
            double kth = -2 * k * Math.PI / n;
            Complex wk = new Complex(Math.cos(kth), Math.sin(kth)); // “kernel” for kth term is the output (based on nth root of unity)
            if (r[k] == null) {
                r[k] = new Complex(1); // exception handling
            }
            if (q[k] == null) {
                q[k] = new Complex(1); // exception handling
            }
            y[k] = q[k].add(wk.multiply(r[k])); // kth term will be addition of odd and even terms
            y[k + n / 2] = q[k].subtract(wk.multiply(r[k])); // (k + n/2)th term will be subtraction of odd and even terms
        }
        return y; // rsultant array
    }

This is a classic implementation of Fast Fourier Transform. We divide the samples of input into odd and even placed terms and call the same function recursively until there is only one term left. After that we use nth (n being the number of samples) complex root of  unity, we combine the results of odd termed fft() and even termed fft() to get the final output. Since at each iteration we are breaking the input into half it will run for O(logN) time and to merge the odd and even termed output we run a loop in each iteration on the O(N). So the total complexity would be O(NlogN), and since it might take longer to compute the fourier transform for large input we require it to be inside the AsyncTask and not on the main thread.

There are many other functionalities throughout the app, where AsyncTask has been used. In a nutshell, AsyncTask is a very useful method to handle longer tasks off the main thread. 

Resources

Tags: PSLab, Android, GSoC 19, AsyncTask, Threading

Continue Reading Examples of how AsyncTask is used in PSLab Android App

Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

In Eventyay Attendee, searching for events has always been a core function that we focus on. When searching for events based on location, autosuggestion based on user input really comes out as a great feature to increase the user experience. Let’s take a look at the implementation

  • Why using Mapbox?
  • Integrating places autosuggestion for searching
  • Conclusion
  • Resources

WHY USING MAPBOX?

There are many Map APIs to be taken into consideration but we choose Mapbox as it is really to set up and use, good documentation and reasonable pricing for an open-source project compared to other Map API.

INTEGRATING PLACES AUTOSUGGESTION FOR SEARCHING

Step 1: Setup dependency in the build.gradle + the MAPBOX key

//Mapbox java sdk
implementation 'com.mapbox.mapboxsdk:mapbox-sdk-services:4.8.0'

Step 2: Set up functions inside ViewModel to handle autosuggestion based on user input:

private fun loadPlaceSuggestions(query: String) {
   // Cancel Previous Call
   geoCodingRequest?.cancelCall()
   doAsync {
       geoCodingRequest = makeGeocodingRequest(query)
       val list = geoCodingRequest?.executeCall()?.body()?.features()
       uiThread { placeSuggestions.value = list }
   }
}

private fun makeGeocodingRequest(query: String) = MapboxGeocoding.builder()
   .accessToken(BuildConfig.MAPBOX_KEY)
   .query(query)
   .languages("en")
   .build()

Based on the input, the functions will update the UI with new inputs of auto-suggested location texts. The MAPBOX_KEY can be given from the Mapbox API.

Step 3: Create an XML file to display autosuggestion strings item and set up RecyclerView in the main UI fragment

Step 4: Set up ListAdapter and ViewHolder to bind the list of auto-suggested location strings. Here, we use CamenFeature to set up with ListAdapter as the main object. With the function .placeName(), information about the location will be given so that ViewHolder can bind the data

class PlaceSuggestionsAdapter :
   ListAdapter<CarmenFeature,
       PlaceSuggestionViewHolder>(PlaceDiffCallback()) {

   var onSuggestionClick: ((String) -> Unit)? = null

   override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PlaceSuggestionViewHolder {
       val itemView = LayoutInflater.from(parent.context)
           .inflate(R.layout.item_place_suggestion, parent, false)
       return PlaceSuggestionViewHolder(itemView)
   }

   override fun onBindViewHolder(holder: PlaceSuggestionViewHolder, position: Int) {
       holder.apply {
           bind(getItem(position))
           onSuggestionClick = [email protected]
       }
   }

   class PlaceDiffCallback : DiffUtil.ItemCallback<CarmenFeature>() {
       override fun areItemsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
           return oldItem.placeName() == newItem.placeName()
       }

       override fun areContentsTheSame(oldItem: CarmenFeature, newItem: CarmenFeature): Boolean {
           return oldItem.equals(newItem)
       }
   }
}
fun bind(carmenFeature: CarmenFeature) {
   carmenFeature.placeName()?.let {
       val placeDetails = extractPlaceDetails(it)
       itemView.placeName.text = placeDetails.first
       itemView.subPlaceName.text = placeDetails.second
       itemView.subPlaceName.isVisible = placeDetails.second.isNotEmpty()

       itemView.setOnClickListener {
           onSuggestionClick?.invoke(placeDetails.first)
       }
   }
}

Step 5: Set up RecyclerView with Adapter created above:

private fun setupRecyclerPlaceSuggestions() {
   rootView.rvAutoPlaces.layoutManager = LinearLayoutManager(context)
   rootView.rvAutoPlaces.adapter = placeSuggestionsAdapter

   placeSuggestionsAdapter.onSuggestionClick = {
       savePlaceAndRedirectToMain(it)
   }
}

RESULTS

CONCLUSION

Place Autocorrection is a really helpful and interesting feature to include in your next project. With the help of Mapbox SDK, it is really easy to implement to enhance your user experience in your application.

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee PR: #1594 – feat: Mapbox Autosuggest

Documentation: https://docs.mapbox.com/android/plugins/overview/places/

Continue Reading Implementing places autosuggestion with Mapbox for searching events in Eventyay Attendee

How to use and implement Save Wave Configs feature in Pocket Science Lab Wave Generator

What is a Wave Generator?

A Wave Generator is one of the most important features of PSLab. It is used to generate different kinds of waves like, sine, triangular, square, PWM. Wave generator UI is as under:

  (Figure 1 : Wave Generator Analog Mode UI)
  (Figure 2 : Wave Generator Digital Mode UI)

As can be seen the Screenshot above user is provided with options to set Frequency, Phase, Duty of different waves and once configurations are set user can either output the waves in Oscilloscope or can compare different waves in Logic Analyzer.

What is Save Wave Configs Feature?


        (Figure 3 : Wave Generator Control Buttons (View,Save,Mode))

In this feature, the user is given a ”Save” button to use this feature. 

The reason to add this feature is that, sometimes we need to perform the same experiment multiple times, is such scenarios if we have to set wave configurations everytime, it will become boring and there will be chances of errors. Hence using the save configs feature, user can currently set configurations in the Local Storage and can use it anytime later. 

Further since the Wave Configurations are saved on Local Storage as .CSV file, a user can save configs and can share the file with others so others can as well set their device to same configurations. The saved Wave Configurations can be seen in the DataLogger Activity and opening a saved log would take the user to Wave Generator Activity where all the configs will be set as per the saved log.

A sample CSV of the log data can be seen below.


(Figure 4: Wave Configs CSV file)

How is Save Configs Feature Implemented

The implementation of this feature is quite simple. There is a class named WaveData.  With the parameters of Mode(Square or PWM), Wave name, Shape, Freq, Phase and Duty. Whenever the user clicks the save configs button, the saveWaveConfigs()  function is called. This function fetches set values of different fields and creates realm objects and also write them to csv file as shown above. Once the realm objects are created, this log can be seen in the Data Logger Activity. The code to generate the realm object for the wave configs (that is the implementation of the function saveWaveConfig()) is given below.

public void saveWaveConfig(View view) {
        long block = System.currentTimeMillis();
        csvLogger.prepareLogFile();
          csvLogger.writeMetaData(getResources().getString(R.string.wave_generator));
        long timestamp;
        double lat, lon;
        String data = "Timestamp,DateTime,Mode,Wave,Shape,Freq,Phase,Duty,lat,lon\n";
        recordSensorDataBlockID(new SensorDataBlock(block, getResources().getString(R.string.wave_generator)));

So till now in the function, we create a header string for the data to be stored in the csv file. We create a block from the current system time. This block will be used to save all the realm object for this function, so all the objects created at this instance will be grouped as a single log entry in DataLoggerActivity.

double freq1 = (double) (WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.FREQUENCY));
double freq2 = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.FREQUENCY);
double phase = (double) WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.PHASE);

String waveType1 = WaveGeneratorCommon.wave.get(WaveConst.WAVE1).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";
String waveType2 = WaveGeneratorCommon.wave.get(WaveConst.WAVE2).get(WaveConst.WAVETYPE) == SIN ? "sine" : "tria";

timestamp = System.currentTimeMillis();
String timeData = timestamp + "," + CSVLogger.FILE_NAME_FORMAT.format(new Date(timestamp));
String locationData = lat + "," + lon;

Next, in the function we get currently set Frequency for both analog waves and phase in the variables. We also store the selected wave shape for each of the waves. Since each entry in the csv file is required to have a timestamp and a location stamp,here we create common stamps of both types and will append it to each entry further in the function. 

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Here we check whether the currently selected mode is Analog(Square) or Digital (PWM). Above code snippet is for the SQUARE mode block. We create WaveGeneratorData object for both SI1 and SI2 waves based on the parameters we stored earlier. We also append the data to a string, data.  Which we will later use to write the log into a csv file.

else {
   double freqSqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.FREQUENCY);
   double dutySqr1 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR1).get(WaveConst.DUTY) / 100;
   double dutySqr2 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.DUTY)) / 100;
   double phaseSqr2 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR2).get(WaveConst.PHASE) / 360;
   double dutySqr3 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.DUTY)) / 100;
   double phaseSqr3 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR3).get(WaveConst.PHASE) / 360;
   double dutySqr4 = ((double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.DUTY)) / 100;
   double phaseSqr4 = (double) WaveGeneratorCommon.wave.get(WaveConst.SQR4).get(WaveConst.PHASE) / 360;

 data += timeData + ",PWM,Sq1,PWM," + String.valueOf(freqSqr1) + ",0," + String.valueOf(dutySqr1) + "," + locationData + "\n";

 recordSensorData(new WaveGeneratorData(timestamp, block, "PWM", "Sq1", "PWM", String.valueOf(freqSqr1), "0", String.valueOf(dutySqr1), lat, lon));
}

The above code snippet shows a block of the condition when the selected mode is PWM. Here we store the set values of Freq, Phase and Duty for each SQ1, SQ2, SQ3 and SQ4 waves into variables. Once we store the values we create WaveGeneratorData objects for each of the waves and also append the data to the data string to write to the csv. The code above includes details only for SQ1, but exact same procedure is followed for SQ2, SQ3, and SQ4. One we have all the data appended to the string we call the following function to write the data to csv file. 

 csvLogger.writeCSVFile(data);

We can see that this function basically stores the current set values of different params into a WaveData object. For each of the waveforms in selected mode (analog/digital), a new instance of WaveData object is created and stored into realm.

When the user opens one of the logs, setReceivedData() function is called in WaveGeneratorActivity. This function iterates on the received realm objects and based on the attributes of each object the data is set in the UI automatically. The implementation of this function is given below, 

public void setReceivedData() {
        for (WaveGeneratorData data : recordedWaveData) {
            Log.d("data", data.toString());
            if (data.getMode().equals(MODE_SQUARE)) {
                WaveGeneratorCommon.mode_selected = WaveConst.SQUARE;
                switch (data.getWave()) {
                    case "Wave1":
                        if (data.getShape().equals("sine")) {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, SIN);
                        } else {
                            WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.WAVETYPE, TRIANGULAR);
                        }
                        WaveGeneratorCommon.wave.get(WaveConst.WAVE1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        break;
                }
                enableInitialState();
            } 

This function iterates over the received WaveGeneratorData objects. For each object we check what is the mode of the waveData. The above code snippet is used when the mode is SQUARE. We get the waveType from the object, and since for SQUARE mode there are only 2 types : Wave1 and Wave2, we set the attributes for each wave as we get them from the objects using WaveGeneratorCommon

else if (data.getMode().equals(MODE_PWM)) {
                WaveGeneratorCommon.mode_selected = WaveConst.PWM;
                switch (data.getWave()) {
                    case "Sq1":
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.FREQUENCY, Double.valueOf(data.getFreq()).intValue());
                        WaveGeneratorCommon.wave.get(WaveConst.SQR1).put(WaveConst.DUTY, ((Double) (Double.valueOf(data.getDuty()) * 100)).intValue());
                        break;
                }
                enableInitialStatePWM();
            }
        }

Same as before if the mode of the object is PWM, there will be 4 cases : SQ1, SQ2, SQ3 and SQ4. And depending on the data stored in the received objects.

In a nutshell this features enables to save and reuse wave configuration with ease. 

A small video to explain the whole functionality of this feature can be found here. 

References

Write to a file in Android

Code Repository

PSLab Android

Tags

PSLab, Wave Generator, SaveConfig, Android, GSoC 19

Continue Reading How to use and implement Save Wave Configs feature in Pocket Science Lab Wave Generator

Data Binding with Kotlin in Eventyay Attendee

Databinding is a common and powerful technique in Android Development. Eventyay Attendee has found many situations where data binding comes in as a great solution for our complex UI. Let’s take a look at this technique.

  • Problems without data binding in Android Development
  • Implementing Databinding with Kotlin inside Fragment
  • Implementing Databinding with Kotlin inside RecyclerView/Adapter
  • Results and GIF
  • Conclusions

PROBLEMS WITHOUT DATABINDING IN ANDROID DEVELOPMENT

Getting the data and fetching it to the UI is a basic work in any kind of application. With Android Development, the most common way to do is it to call function like .setText(), isVisible = True/False,.. in your fragment. This can create many long boilerplate codes inside Android classes. Databinding removes them and moves to the UI classes (XML).

IMPLEMENTING DATABINDING IN FRAGMENT VIEW

Step 1: Enabling data binding in the project build.gradle

android {
   dataBinding {
       enabled = true
   }

Step 2: Wrap the current layout with <layout></layout> tag. Inside that, put <data></data> to indicate any variables needed for data binding. For example, this code here display an event variable for our fragment about event details:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:bind="http://schemas.android.com/tools">

   <data>

       <variable
           name="event"
           type="org.fossasia.openevent.general.event.Event" />
   </data>

   <androidx.coordinatorlayout.widget.CoordinatorLayout
       android:id="@+id/eventCoordinatorLayout"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:background="@android:color/white">

Step 3: Bind your data in the XML file and create a Binding Adapter class for better usage

With the setup above, you can start binding your data with “@{<data code here>}”

<TextView
   android:id="@+id/eventName"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:layout_marginLeft="@dimen/layout_margin_large"
   android:layout_marginTop="@dimen/layout_margin_large"
   android:layout_marginRight="@dimen/layout_margin_large"
   android:text="@{event.name}"
   android:fontFamily="sans-serif-light"
   android:textColor="@color/dark_grey"
   android:textSize="@dimen/text_size_extra_large"
   app:layout_constraintEnd_toEndOf="@+id/eventImage"
   app:layout_constraintStart_toStartOf="@+id/eventImage"
   app:layout_constraintTop_toBottomOf="@+id/eventImage"
   tools:text="Open Source Meetup" />

Sometimes, to bind our data normally we need to use a complex function, then creating Binding Adapter class really helps. For example, Eventyay Attendee heavily uses Picasso function to fetch image to ImageView:

@BindingAdapter("eventImage")
fun setEventImage(imageView: ImageView, url: String?) {
   Picasso.get()
       .load(url)
       .placeholder(R.drawable.header)
       .into(imageView)
}
<ImageView
   android:id="@+id/eventImage"
   android:layout_width="@dimen/layout_margin_none"
   android:layout_height="@dimen/layout_margin_none"
   android:scaleType="centerCrop"
   android:transitionName="eventDetailImage"
   app:layout_constraintDimensionRatio="2"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintHorizontal_bias="0.5"
   app:layout_constraintStart_toStartOf="parent"
   app:eventImage="@{event.originalImageUrl}"
   app:layout_constraintTop_toBottomOf="@id/alreadyRegisteredLayout" />

Step 4: Finalize data binding setup in Android classes. We can create a binding variable. The binding root will serve as the root node of the layout. Whenever data is needed to be bind, set the data variable stated to that binding variable and call function executePendingBingdings()

private lateinit var rootView: View
private lateinit var binding: FragmentEventBinding
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_event, container, false)
rootView = binding.root
binding.event = event
binding.executePendingBindings()

SOME NOTES

  • In the example mentioned above, the name of the binding variable class is auto-generated based on the name of XML file + “Binding”. For example, the XML name was fragment_event so the DataBinding classes generated name is FragmentEventBinding.
  • The data binding class is only generated only after compiling the project.
  • Sometimes, compiling the project fails because of some problems due to data binding without any clear log messages, then that’s probably because of error when binding your data in XML class. For example, we encounter a problem when changing the value in Attendee data class from firstname to firstName but XML doesn’t follow the update. So make sure you bind your data correctly
<TextView
   android:id="@+id/name"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_marginBottom="@dimen/layout_margin_large"
   android:textColor="@color/black"
   android:textSize="@dimen/text_size_expanded_title_large"
   android:text="@{attendee.firstname + ` ` + attendee.lastname}"
   tools:text="@string/name_preview" />

CONCLUSION

Databinding is the way to go when working with a complex UI in Android Development. This helps reducing boilerplate code and to increase the readability of the code and the performance of the UI. One problem with data binding is that sometimes, it is pretty hard to debug with an unhelpful log message. Hopefully, you can empower your UI in your project now with data binding.  

RESOURCES

Eventyay Attendee Android Codebase: https://github.com/fossasia/open-event-android

Eventyay Attendee Android PR: #1961 – feat: Set up data binding for Recycler/Adapter

Documentation: https://developer.android.com/topic/libraries/data-binding

Google Codelab: https://codelabs.developers.google.com/codelabs/android-databinding/#0

Continue Reading Data Binding with Kotlin in Eventyay Attendee

Implementing a Splash Screen, the wiser way

  • Post author:
  • Post category:FOSSASIA

Implementing a Splash Screen, the wiser way

What is a Splash Screen?

A Splash Screen is basically a nice intro-screen that mobile applications have on startup of the app on a device. The splash screen can be customized according to the app’s UX need-animations, sound effects, etc. are some common tweaks to a simple splash screen.

I have been working with FOSSASIA on the Neurolab Android App where we made a splash screen for the same. Our implemented splash screen is below:

                                                     Neurolab Splash Screen

While developing this, we followed Google Material Design guidelines and the pattern it suggests is termed as ‘Launch Screen’. Displaying a launch screen can decrease the sense of long load time, and has the potential to add delight to the user experience. Launch screen implementation is considered as one of the best-practised development skills for a proper splash screen for an app.

Implementation 

Now, it is not a good idea to use a splash screen that wastes a user’s time. This should be strictly avoided. The right way of implementing a splash screen is a little different. In the new approach specify your splash screen’s background as the activity’s theme background. This way, we can effectively and efficiently use the time gap between the startup of the app and the onCreate() method.

In the Neurolab app, we use the splash screen as a bridge for the time gap between the app startup when we click the app icon and the onCreate method of the Neurolab Activity (Main/Launcher Screen) of the app, wherein the various UI components are laid out on the screen and the functionalities, navigations, listeners are linked to those components.

So, here we won’t be creating a new layout for the Splash screen as a separate activity. Rather we would specify the theme of the landing activity as the splash screen.

We create a drawable named splash_screen.xml in our project and give a parent tag of layer-list. Here is the code for our drawable file:

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque">
    <item android:drawable="@android:color/white" />
    <item>
        <bitmap
            android:gravity="center_horizontal"
            android:src="@drawable/splash_image" />
    </item>
</layer-list>

Next, we are going to create a new theme in the styles resource file. This theme is going to be used as the base theme for the main activity screen of the app. In this style, we specify our created drawable file to the property name windowBackground.

<style name="AppTheme.Launcher">
        <item name="android:windowBackground">@drawable/splash_screen</item>
</style>

Then, update this style in the project manifest file to set the theme of the main activity

android:theme="@style/AppTheme.Launcher"

Having done the steps so far, we create a simple class extending the AppCompatActivity. Note- This may seem like another Activity screen, but it is not. We don’t specify the setContentView() here. Instead of this class just directs to the main/home activity using an Intent. Finally, be sure to finish() the SplashActivity activity (class) to remove prevailing unused/idle activities from back stack.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Start landing activity screen
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
}

We are done!!

Launch your app, and observe your Launch screen. I can promise you that your “time won’t be wasted”(pun intended).

Thanks for reading. Hope this adds value to your Android application development skills. 

References:

Tags: FOSSASIA. Neurolab, GSOC19, Open-source, splash-screen, Android

Continue Reading Implementing a Splash Screen, the wiser way

Serializing Java objects for REST API Requests in Open Event Organizer App

Open Event Organizer App is a client side application which uses REST API for network requests. The server supports sending and receiving of data only in JSONAPI spec, so, we needed to serialize java models into JSON objects and deserialize JSON data into java models following JSONAPI spec. To achieve this we followed the following steps.

Specifications

We will be using jasminb/jsonapi-converter which handles request/response parsing of models following JSONAPI Spec and Retrofit plugin of jackson converter to serializing JSON to Java Models and vice versa.

Let’s create a java model. We are using some annotations provided by Lombok library to avoid writing boilerplate code. @JsonNaming annotation is used to apply KebabCaseStrategy while serializing fields

@Data
@Type(“order”)
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategy.KebabCaseStrategy.class)
@Table(database = OrgaDatabase.class, allFields = true)
public class Order {

@PrimaryKey
@Id(LongIdHandler.class)
public Long id;

public float amount;
public String completedAt;
public String identifier;
public String paidVia;
public String paymentMode;
public String status;

@Relationship(“event”)
@ForeignKey(stubbedRelationship = true, onDelete = ForeignKeyAction.CASCADE)
public Event event;

public Order() { }
}

In the NetworkModule class, there is a method providesMappedClasses() containing a list of classes that needs to be serialized/deserialized. We need to add the above model in the list. Then, this list is provided to Singleton instance of JSONAPIConvertorFactory through Dagger. JSONAPIConvertorFactory uses the Retrofit ObjectMapper and maps the classes that are handled by this instance.

@Provides
Class[] providesMappedClasses() {
return new Class[]{Event.class, Attendee.class, Ticket.class, Order.class};
}

Further, various serialization properties can be used while building Singleton ObjectMapper instance. Adding any properties here ensures that these are applied to all the mapped classes by JSONAPIConvertorFactory. For eg, we are using the serialization property to throw an exception and fail whenever empty beans are encountered.

@Provides
@Singleton
ObjectMapper providesObjectMapper() {
return new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
// Handle constant breaking changes in API by not including null fields
// TODO: Remove when API stabilizes and/or need to include null values is there
.setSerializationInclusion(JsonInclude.Include.NON_ABSENT);
}

Resources

  1. Github Repository for jsonapi-converter https://github.com/jasminb/jsonapi-converter
  2. Github repository for Jackson Retrofit Plugin https://github.com/square/retrofit/tree/master/retrofit-converters/jackson
  3. Official Website for Project Lombok https://projectlombok.org/

Github Repository for Open-Event-Orga-App https://github.com/fossasia/open-event-orga-app

Continue Reading Serializing Java objects for REST API Requests in Open Event Organizer App

Adding Preference Settings using Preference Fragment Compat

It is very much likely that one needs to add preferences to their app which span the entire application and therefore can be accessed anywhere in the app without storing anything in database or making global variables. For an instance, in Open Event Organizer App we added the preferences to store the privacy policy, cookie policy etc. The user can access these items in Settings Preference which in device settings. In this blog post we will see how to add preference settings to the app by storing the data in shared preferences.

Specifications

The benefit of storing the data in shared preference and not in local storage is that the access time for the data is drastically reduced and the data persists even when the app is closed. We will use this library which is built on top of official preference-v7 library.

Firstly, we will make a preference resource layout file and add the preference for privacy policy and cookie policy in the preference screen.

<PreferenceScreen xmlns:android=”http://schemas.android.com/apk/res/android”>

<Preference
android:key=”@string/privacy_policy_key”
android:title=”@string/privacy_policy” />

<Preference
android:key=”@string/cookie_policy_key”
android:title=”@string/cookie_policy” />

</PreferenceScreen>

Make a separate preference fragment class named LegalPreferenceFragment which extends PreferenceFragmentCompat. Then we will override onCreatePreferenceFix() method.

Inside this, we will create an instance of Preference Manager and set shared preference name for it and set the preference using the layout file. This enables us to use findPreference() method to retrieve the layout preferences by their key. After, retrieving the preference we will set onClick listener to launch activity with an intent to open browser for the url passed in data bundle.

@Override
public void onCreatePreferencesFix(@Nullable Bundle bundle, String rootKey) {
PreferenceManager manager = getPreferenceManager();
manager.setSharedPreferencesName(Constants.FOSS_PREFS);

setPreferencesFromResource(R.xml.legal_preferences, rootKey);

findPreference(getString(R.string.privacy_policy_key)).setOnPreferenceClickListener(preference -> {
BrowserUtils.launchUrl(getContext(), PRIVACY_POLICY_URL);
return true;
});
findPreference(getString(R.string.cookie_policy_key)).setOnPreferenceClickListener(preference -> {
BrowserUtils.launchUrl(getContext(), COOKIE_POLICY_URL);
return true;
});
}

References

  1. Preference Fragment Compat library by Takisoft https://github.com/Gericop/Android-Support-Preference-V7-Fix
  2. Android Preference Documentation https://developer.android.com/reference/android/preference/PreferenceGroup
Continue Reading Adding Preference Settings using Preference Fragment Compat