com.embeddedlog.LightUpDroid.LightUpPiSync.java Source code

Java tutorial

Introduction

Here is the source code for com.embeddedlog.LightUpDroid.LightUpPiSync.java

Source

/*
 * Copyright (C) 2015 carlosperate http://carlosperate.github.io
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.embeddedlog.LightUpDroid;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.widget.Toast;

import com.embeddedlog.LightUpDroid.provider.Alarm;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Synchronises the Alarms with the LightUpPi server alarms by providing methods to retrieve, edit,
 * add and delete alarms on the server.
 */
public class LightUpPiSync {
    private static final String LOG_TAG = "LightUpPiSync: ";

    private Context mActivityContext;
    private AlarmClockFragment mAlarmFragment;

    // Used to schedule a permanently running background LightUpPi server check
    private ScheduledExecutorService scheduleServerCheck;

    // Defines the types of tasks that is required to be performed
    public enum TaskType {
        SYNC, PUSH_TO_PHONE, PUSH_TO_SERVER, GET_ALARM, ADD_ALARM, EDIT_ALARM, DELETE_ALARM,
    }

    /**
     * Public constructor. Saves the class context to be able to check the network connectivity
     * and display a progress dialog.
     *
     * @param activityContext Context of the activity (no application context) requesting the sync.
     */
    public LightUpPiSync(Context activityContext, String alarmFragmentTag) {
        this.mActivityContext = activityContext;
        Activity activity = (Activity) this.mActivityContext;
        this.mAlarmFragment = (AlarmClockFragment) activity.getFragmentManager()
                .findFragmentByTag(alarmFragmentTag);
    }

    /**
     * Synchronisation procedure to push all alarms from the LightUpPi server onto the phone.
     */
    public void syncPushToPhone() {
        Uri.Builder allAlarmsUri = getServerUriBuilder();
        allAlarmsUri.appendPath("getAlarm").appendQueryParameter("id", "all");
        getJsonHandler(allAlarmsUri, TaskType.PUSH_TO_PHONE, Alarm.INVALID_ID);
    }

    private void syncPushToPhoneCallback(JSONObject jAllAlarms) {
        JSONArray jAlarms;
        List<Alarm> serverAlarms = new LinkedList<Alarm>();
        try {
            jAlarms = jAllAlarms.getJSONArray("alarms");
            for (int i = 0; i < jAlarms.length(); i++) {
                serverAlarms.add(alarmFromJson(jAlarms.getJSONObject(i)));
                if (Log.LOGV)
                    Log.v("Alarm from server: " + serverAlarms.get(i).toString());
            }
        } catch (Exception e) {
            if ((e instanceof JSONException) || (e instanceof NullPointerException)) {
                launchToast(R.string.lightuppi_sync_fail);
                Log.w(LOG_TAG + "Exception reading callback from push to phone operation: " + e);
                return;
            } else {
                throw new RuntimeException(e);
            }
        }

        // Get local alarms, passing null as selection argument retrieves all
        ContentResolver cr = mActivityContext.getContentResolver();
        List<Alarm> localAlarms = Alarm.getAlarms(cr, null);

        // Now we have lists of all the alarms, because we are pushing to the phone check the
        // current local alarms and remove them if they are not in the list
        for (Alarm localAlarm : localAlarms) {
            toNextLocalAlarmIteration: {
                for (Alarm serverAlarm : serverAlarms) {
                    if (localAlarm.lightuppiId == serverAlarm.lightuppiId) {
                        // Because we are pushing to the phone update the alarm to whatever is in
                        // the server, including the server timestamp
                        copyAndroidProperties(localAlarm, serverAlarm);
                        mAlarmFragment.asyncUpdateAlarm(serverAlarm, false, true);
                        serverAlarms.remove(serverAlarm);
                        break toNextLocalAlarmIteration;
                    }
                }
                // This is only executed if a match between server and local alarm was not found
                mAlarmFragment.asyncDeleteAlarm(localAlarm, null, true);
            }
        }
        // The rest of the serverAlarms are new to the phone and present in the server
        for (Alarm serverAlarm : serverAlarms) {
            mAlarmFragment.asyncAddAlarm(serverAlarm, true);
        }
    }

    /**
     * Adds an alarm to the LightUpPi server.
     *
     * @param alarm New Alarm to add to LightUpPi server.
     */
    public void addServerAlarm(Alarm alarm) {
        // First check if alarm has no associated LightUpPi server ID
        if (alarm.lightuppiId == Alarm.INVALID_ID) {
            Uri.Builder addAlarmUri = getServerUriBuilder();
            addAlarmUri.appendPath("addAlarm").appendQueryParameter("hour", Integer.toString(alarm.hour))
                    .appendQueryParameter("minute", Integer.toString(alarm.minutes))
                    .appendQueryParameter("monday", Boolean.toString(alarm.daysOfWeek.isMondayEnabled()))
                    .appendQueryParameter("tuesday", Boolean.toString(alarm.daysOfWeek.isTuesdayEnabled()))
                    .appendQueryParameter("wednesday", Boolean.toString(alarm.daysOfWeek.isWednesdayEnabled()))
                    .appendQueryParameter("thursday", Boolean.toString(alarm.daysOfWeek.isThursdayEnabled()))
                    .appendQueryParameter("friday", Boolean.toString(alarm.daysOfWeek.isFridayEnabled()))
                    .appendQueryParameter("saturday", Boolean.toString(alarm.daysOfWeek.isSaturdayEnabled()))
                    .appendQueryParameter("sunday", Boolean.toString(alarm.daysOfWeek.isSundayEnabled()))
                    .appendQueryParameter("enabled", Boolean.toString(alarm.enabled))
                    .appendQueryParameter("label", alarm.label)
                    .appendQueryParameter("timestamp", Long.toString(alarm.timestamp));
            getJsonHandler(addAlarmUri, TaskType.ADD_ALARM, alarm.id);
        } else {
            launchToast(R.string.lightuppi_add_existing);
        }
    }

    private void addServerAlarmCallback(long alarmID, JSONObject jResult) {
        boolean addSuccess;
        long lightuppiId;
        try {
            addSuccess = jResult.getBoolean("success");
            lightuppiId = jResult.getLong("id");
        } catch (Exception e) {
            if ((e instanceof JSONException) || (e instanceof NullPointerException)) {
                Log.w(LOG_TAG + "Exception when reading callback from add operation: " + e);
                launchToast(R.string.lightuppi_add_unsuccessful);
                return;
            } else {
                throw new RuntimeException(e);
            }
        }
        if (addSuccess) {
            // We need the alarm back before we can edit the LightUpPi ID
            ContentResolver cr = mActivityContext.getContentResolver();
            Alarm addedAlarm = Alarm.getAlarm(cr, alarmID);
            addedAlarm.lightuppiId = lightuppiId;
            // Last argument causes the bypass of the Alarm.updateAlarm() automatic timestamp
            // and the edit of the alarm in the LightUpPi server
            mAlarmFragment.asyncUpdateAlarm(addedAlarm, false, true);
            launchToast(R.string.lightuppi_add_successful);
        } else {
            launchToast(R.string.lightuppi_add_unsuccessful);
        }
    }

    /**
     * Edits an alarm from the LightUpPi server.
     *
     * @param alarm LightUpPi Alarm to edit.
     */
    public void editServerAlarm(Alarm alarm) {
        // First check if alarm has an associated LightUpPi server ID
        if (alarm.lightuppiId != Alarm.INVALID_ID) {
            Uri.Builder editAlarmUri = getServerUriBuilder();
            editAlarmUri.appendPath("editAlarm").appendQueryParameter("id", Long.toString(alarm.lightuppiId))
                    .appendQueryParameter("hour", Integer.toString(alarm.hour))
                    .appendQueryParameter("minute", Integer.toString(alarm.minutes))
                    .appendQueryParameter("monday", Boolean.toString(alarm.daysOfWeek.isMondayEnabled()))
                    .appendQueryParameter("tuesday", Boolean.toString(alarm.daysOfWeek.isTuesdayEnabled()))
                    .appendQueryParameter("wednesday", Boolean.toString(alarm.daysOfWeek.isWednesdayEnabled()))
                    .appendQueryParameter("thursday", Boolean.toString(alarm.daysOfWeek.isThursdayEnabled()))
                    .appendQueryParameter("friday", Boolean.toString(alarm.daysOfWeek.isFridayEnabled()))
                    .appendQueryParameter("saturday", Boolean.toString(alarm.daysOfWeek.isSaturdayEnabled()))
                    .appendQueryParameter("sunday", Boolean.toString(alarm.daysOfWeek.isSundayEnabled()))
                    .appendQueryParameter("enabled", Boolean.toString(alarm.enabled))
                    .appendQueryParameter("label", alarm.label);
            getJsonHandler(editAlarmUri, TaskType.EDIT_ALARM, alarm.id);
        } else {
            launchToast(R.string.lightuppi_no_server_ID);
        }
    }

    private void editServerAlarmCallback(JSONObject jResult) {
        boolean editSuccess;
        long lightuppiId;
        long newTimestamp;
        try {
            editSuccess = jResult.getBoolean("success");
            lightuppiId = jResult.getLong("id");
            newTimestamp = jResult.getLong("timestamp");
        } catch (Exception e) {
            if ((e instanceof JSONException) || (e instanceof NullPointerException)) {
                Log.w(LOG_TAG + "Exception when reading callback from edit operation: " + e);
                launchToast(R.string.lightuppi_edit_unsuccessful);
                return;
            } else {
                throw new RuntimeException(e);
            }
        }
        if (editSuccess) {
            // We need the alarm back before we can edit the timestamp
            ContentResolver cr = mActivityContext.getContentResolver();
            Alarm editedAlarm = Alarm.getAlarmLightuppiId(cr, lightuppiId);
            editedAlarm.timestamp = newTimestamp;
            // Last argument causes the bypass of the Alarm.updateAlarm() automatic timestamp
            // and the edit of the alarm in the LightUpPi server
            mAlarmFragment.asyncUpdateAlarm(editedAlarm, false, true);
            launchToast(R.string.lightuppi_edit_successful);
        } else {
            launchToast(R.string.lightuppi_edit_unsuccessful);
        }
    }

    /**
     * Deletes an alarm from the LightUpPi server.
     *
     * @param alarm LightUpPi Alarm to delete.
     */
    public void deleteServerAlarm(Alarm alarm) {
        // First check if alarm has an associated LightUpPi server ID
        if (alarm.lightuppiId != Alarm.INVALID_ID) {
            Uri.Builder deleteAlarmUri = getServerUriBuilder();
            deleteAlarmUri.appendPath("deleteAlarm").appendQueryParameter("id", Long.toString(alarm.lightuppiId));
            getJsonHandler(deleteAlarmUri, TaskType.DELETE_ALARM, alarm.id);
        } else {
            launchToast(R.string.lightuppi_no_server_ID);
        }
    }

    private void deleteServerAlarmCallback(JSONObject jResult) {
        boolean deleteSuccess;
        try {
            deleteSuccess = jResult.getBoolean("success");
        } catch (Exception e) {
            if ((e instanceof JSONException) || (e instanceof NullPointerException)) {
                Log.w(LOG_TAG + "Exception when reading callback from delete operation: " + e);
                launchToast(R.string.lightuppi_delete_unsuccessful);
                return;
            } else {
                throw new RuntimeException(e);
            }
        }
        if (deleteSuccess) {
            launchToast(R.string.lightuppi_delete_successful);
        } else {
            launchToast(R.string.lightuppi_delete_unsuccessful);
        }
    }

    /**
     * Gets the LightUpPi server IP from the settings and returns the server address string.
     *
     * @return Sever address to the LightUpPi app root folder.
     */
    private Uri.Builder getServerUriBuilder() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mActivityContext);
        String serverIP = prefs.getString(SettingsActivity.KEY_LIGHTUPPI_SERVER, "");
        // The LightUpPi server application runs through the LightUpPi directory
        Uri.Builder serverUriBuilder = new Uri.Builder();
        serverUriBuilder.scheme("http").authority(serverIP).appendPath("LightUpPi");
        if (Log.LOGV)
            Log.v(LOG_TAG + "LightUpPi server " + serverUriBuilder.build().toString());
        return serverUriBuilder;
    }

    /**
     * Every request is handled by this method, which launches an async task to retrieve the data.
     * Before attempting to fetch the URL, makes sure that there is a network connection.
     *
     * @param uriBuilder The URI Builder of the JSON data address to request.
     * @param taskType Indicates which task it is to be performed.
     * @param alarmId If applicable, the Alarm ID (local, not LightUpPi Id) to perform the task.
     */
    private void getJsonHandler(Uri.Builder uriBuilder, TaskType taskType, long alarmId) {
        // First check if there is network connectivity
        ConnectivityManager connMgr = (ConnectivityManager) mActivityContext
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        if (networkInfo != null && networkInfo.isConnected()) {
            String urlString = uriBuilder.build().toString();
            new DownloadJsonTask(taskType, alarmId).execute(urlString);
        } else {
            launchToast(R.string.lightuppi_no_connection);
        }
    }

    /**
     * Uses AsyncTask to create a task away from the main UI thread, where the wrapper class is
     * called from. This task takes a URL string and uses it to create an HttpUrlConnection.
     * Once the connection has been established, the AsyncTask downloads the contents of the
     * web page as an InputStream. Finally, the InputStream is converted into a string, which is
     * displayed in the UI by the AsyncTask's onPostExecute method.
     */
    private class DownloadJsonTask extends AsyncTask<String, Void, JSONObject> {
        private ProgressDialog progress;
        private TaskType mTaskType;
        private long mAlarmId;

        /**
         * Constructor requires a TaskType argument to identify the correct callback.
         *
         * @param taskType The type of task required in order to identify the right callback.
         */
        DownloadJsonTask(TaskType taskType, long alarmId) {
            this.mTaskType = taskType;
            this.mAlarmId = alarmId;
        }

        /**
         * Launches the progress dialog while the data is being retrieved.
         * It is dismissed on onPostExecute.
         */
        @Override
        protected void onPreExecute() {
            ((Activity) mActivityContext).runOnUiThread(new Runnable() {
                public void run() {
                    progress = ProgressDialog.show(mActivityContext, null,
                            mActivityContext.getString(R.string.lightuppi_syncing_message), true);
                }
            });
        }

        /**
         * @param urls String array with the URL to retrieve JSON from, only first array item used.
         * @return JSON from server in JSONObject format.
         */
        @Override
        protected JSONObject doInBackground(String... urls) {
            String jsonStr;
            try {
                // Only expecting 1 url parameter, overwrite requires the array to be maintained
                jsonStr = getJsonFrom(urls[0]);
            } catch (IOException e) {
                Log.w(LOG_TAG + "JSONException: " + e.toString());
                // Error dealt with in the callback from onPostExecute, by passing null object
                return null;
            }
            if (Log.LOGV)
                Log.v(LOG_TAG + jsonStr);
            JSONObject wrapperJsonObject = null;
            try {
                wrapperJsonObject = new JSONObject(jsonStr);
            } catch (JSONException e) {
                Log.w(LOG_TAG + "JSONException: " + e.toString());
                // Error dealt with in the callback from onPostExecute, by passing null object
            }
            return wrapperJsonObject;
        }

        /** Closes the progress dialog and sends the data to the relevant callback. */
        @Override
        protected void onPostExecute(JSONObject result) {
            // Select callback based on task type
            switch (mTaskType) {
            case SYNC:
                break;
            case PUSH_TO_SERVER:
                break;
            case PUSH_TO_PHONE:
                syncPushToPhoneCallback(result);
                break;
            case GET_ALARM:
                break;
            case ADD_ALARM:
                addServerAlarmCallback(mAlarmId, result);
                break;
            case EDIT_ALARM:
                editServerAlarmCallback(result);
                break;
            case DELETE_ALARM:
                deleteServerAlarmCallback(result);
                break;
            default:
                Log.w(LOG_TAG + "Coding bug, there was no callback defined for the task " + mTaskType.toString());
                break;
            }
            // Close the progress dialog if applicable, cases need to be the same as onPreExecute
            progress.dismiss();
        }

        /**
         * Given a URL, establishes an HttpUrlConnection and retrieves the content as a
         * InputStream, which it returns as a string.
         *
         * @param urlStr String array containing as the first argument the URL to retrieve data.
         * @return String with the URL data
         * @throws IOException
         */
        private String getJsonFrom(String urlStr) throws IOException {
            InputStream is = null;
            try {
                URL url = new URL(urlStr);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(3000); /* milliseconds */
                conn.setConnectTimeout(5000); /* milliseconds */
                conn.setRequestMethod("GET");
                conn.setDoInput(true);

                // Starts the query
                conn.connect();
                int response = conn.getResponseCode();
                if (response == 500) {
                    launchToast(R.string.lightuppi_response_500);
                } else if (response != 200) {
                    launchToast(String.format(mActivityContext.getString(R.string.lightuppi_response_not_200),
                            response));
                }

                // Get and convert the InputStream into a string
                is = conn.getInputStream();
                return stringFromStream(is);
            } finally {
                // Ensure InputStream is closed after the app is finished using it.
                if (is != null)
                    is.close();
            }
        }

        /**
         * Converts the input stream from the web content into an String.
         *
         * @param stream InputStream to be converted into String.
         * @return String with the stream parameter data.
         * @throws IOException
         */
        private String stringFromStream(InputStream stream) throws IOException {
            BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
            StringBuilder out = new StringBuilder();
            String newLine = System.getProperty("line.separator");
            String line;
            while ((line = reader.readLine()) != null) {
                out.append(line);
                out.append(newLine);
            }
            return out.toString();
        }
    }

    private Alarm alarmFromJson(JSONObject aJson) {
        // If mSelectedAlarm is null then we're creating a new alarm.
        Alarm alarm = new Alarm();
        alarm.alert = RingtoneManager.getActualDefaultRingtoneUri(mActivityContext, RingtoneManager.TYPE_ALARM);
        if (alarm.alert == null) {
            alarm.alert = Uri.parse("content://settings/system/alarm_alert");
        }
        // Setting the vibrate option to always true, as there is no attribute in LightUpPi
        alarm.vibrate = true;
        // Setting the 'delete after use' option to always false, as there is no such feature in
        // the LightUpPi alarm system and all alarms are repeatable
        alarm.deleteAfterUse = false;

        // Parsing the JSON data
        try {
            alarm.hour = aJson.getInt("hour");
            alarm.minutes = aJson.getInt("minute");
            alarm.enabled = aJson.getBoolean("enabled");
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("monday"), Calendar.MONDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("tuesday"), Calendar.TUESDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("wednesday"), Calendar.WEDNESDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("thursday"), Calendar.THURSDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("friday"), Calendar.FRIDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("saturday"), Calendar.SATURDAY);
            alarm.daysOfWeek.setDaysOfWeek(aJson.getBoolean("sunday"), Calendar.SUNDAY);
            alarm.label = aJson.getString("label");
            alarm.lightuppiId = aJson.getLong("id");
            alarm.timestamp = aJson.getLong("timestamp");
        } catch (JSONException e) {
            Log.w(LOG_TAG + " JSONException: " + e.toString());
            alarm = null;
        }
        return alarm;
    }

    /**
     * Because the LightUpDrop Alarms have more data than the LightUpPi Alarms this method is used
     * to copy the properties over from one alarm to the other.
     *
     * @param droid Alarm local to the phone.
     * @param pi Alarm coming from the LightUpPi server.
     */
    private void copyAndroidProperties(Alarm droid, Alarm pi) {
        pi.id = droid.id;
        pi.alert = droid.alert;
        pi.vibrate = droid.vibrate;
        pi.deleteAfterUse = droid.deleteAfterUse;
    }

    /**
     * Initiates a background thread to check if the LightUpPi server is reachable.
     *
     * @param guiHandler Handler for the activity GUI, for which to send one of the two runnables.
     * @param online Runnable to execute in the Handler if the server is online.
     * @param offline Runnable to execute in the Handler if the server is offline.
     */
    public void startBackgroundServerCheck(final Handler guiHandler, final Runnable online,
            final Runnable offline) {
        // Check for network connectivity
        ConnectivityManager connMgr = (ConnectivityManager) mActivityContext
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        if ((networkInfo != null) && networkInfo.isConnected()
                && ((scheduleServerCheck == null) || scheduleServerCheck.isShutdown())) {
            // Get the ping address
            final Uri.Builder pingUri = getServerUriBuilder();
            pingUri.appendPath("ping");
            // Schedule the background server check
            scheduleServerCheck = Executors.newScheduledThreadPool(1);
            scheduleServerCheck.scheduleWithFixedDelay(new Runnable() {
                public void run() {
                    int response = 0;
                    try {
                        URL url = new URL(pingUri.build().toString());
                        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                        conn.setReadTimeout(3000); /* milliseconds */
                        conn.setConnectTimeout(5000); /* milliseconds */
                        conn.setRequestMethod("GET");
                        conn.setDoInput(true);
                        conn.connect();
                        response = conn.getResponseCode();
                    } catch (Exception e) {
                        // Safely ignored as a response!=200 will trigger the offline title
                    }
                    if (response == 200) {
                        if (Log.LOGV)
                            Log.i(LOG_TAG + "Server response 200");
                        guiHandler.post(online);
                    } else {
                        if (Log.LOGV)
                            Log.i(LOG_TAG + "Server response NOT 200");
                        guiHandler.post(offline);
                    }
                }
            }, 0, 30, TimeUnit.SECONDS);
            if (Log.LOGV)
                Log.v(LOG_TAG + "BackgroundServerCheck started");
        } else {
            if (Log.LOGV)
                Log.d(LOG_TAG + "Server response NOT 200");
            guiHandler.post(offline);
        }
    }

    /** Stops the background server check */
    public void stopBackgroundServerCheck() {
        try {
            scheduleServerCheck.shutdown();
            if (Log.LOGV)
                Log.v(LOG_TAG + "BackgroundServerCheck stopped");
        } catch (NullPointerException e) {
            // This will be triggered due to the network being unavailable, safe to ignore
        }
    }

    /** Launches a Toast in the main gui thread */
    private void launchToast(final int resourceId) {
        ((Activity) mActivityContext).runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(mActivityContext, resourceId, Toast.LENGTH_LONG).show();
            }
        });
    }

    private void launchToast(final String toastText) {
        ((Activity) mActivityContext).runOnUiThread(new Runnable() {
            public void run() {
                Toast.makeText(mActivityContext, toastText, Toast.LENGTH_LONG).show();
            }
        });
    }
}