Android – zastosowanie API kalendarza Google’a

Wykorzystanie API kalendarza Google’a w aplikacjach webowych oraz mobilnych i integracja budowanego rozwiązania z narzędziami Google’a zwiększa możliwości implementowanej aplikacji. Przykładowo w wersji 2.1 aplikacji RecoAlarm dodałem możliwości synchronizacji alarmów z kalendarzem Google, dzięki czemu informacja może znaleźć się nie tylko w aplikacji, lecz również w kalendarzu na smartfonie czy komputerze. Poniżej przedstawię w kilku krokach jak można wykorzystać API kalendarza Google’a.

Dokumentacja i przykłady kodu

Integrację aplikację z kalendarzem Google’a rozpocząłem od zapoznania się z przewodnikiem po API, a także przykładami dla platformy Android, które przedstawiają jak włączyć API w Google Developers Console oraz jak utworzyć czy też zmodyfikować projekt w środowisku Android Studio, by odpowiednie biblioteki i zależności zostały pobrane (plik build.gradle) oraz by aplikacja posiadała wymagane uprawnienia (plik AndroidManifest.xml). W przykładach Google kod w Javie został umieszczony w jednym pliku, natomiast poniżej przedstawię jak w swoim projekcie wprowadziłem zmiany, by kod rozdzielić oraz dostosować go tak, by móc pewne fragmenty wywołań API Google’a wykorzystać nie tylko do kalendarza.

Klasa przygotowująca do wywoływania API Google’a

Obiekt klasy przygotowującej do wywoływania API Google’a może być utworzony np. w Activity, która razem z kontekstem jest przekazywana do konstruktora klasy. Stałe utworzone na początku definicji klasy wykorzystywane są do odczytu i zapisu preferencji (PREF_GOOGLE_ACCOUNT, PREF_GOOGLE_CALENDAR), definiowania uprawnień do API (SCOPES) oraz kodów wykorzystywanych przy otwieraniu okien dialogowych np. do wyboru konta Google (REQUEST_ACCOUNT_PICKER, REQUEST_AUTHORIZATION, REQUEST_GOOGLE_PLAY_SERVICES, REQUEST_PERMISSION_GET_ACCOUNTS).

public class ApiGoogle implements EasyPermissions.PermissionCallbacks {
    Context mContext;
    Activity mActivity;
    GoogleAccountCredential mCredential;

    public static final String PREF_GOOGLE_ACCOUNT = "google_account";
    public static final String PREF_GOOGLE_CALENDAR = "google_calendar";

    private static final String[] SCOPES = { CalendarScopes.CALENDAR };

    public static final int REQUEST_ACCOUNT_PICKER = 1000;
    public static final int REQUEST_AUTHORIZATION = 1001;
    public static final int REQUEST_GOOGLE_PLAY_SERVICES = 1002;
    public static final int REQUEST_PERMISSION_GET_ACCOUNTS = 1003;

    public ApiGoogle(Context context, Activity activity) {
        // Initialize credentials and service object.
        mContext = context;
        mActivity = activity;
        mCredential = GoogleAccountCredential.usingOAuth2(
                context.getApplicationContext(), Arrays.asList(SCOPES))
                .setBackOff(new ExponentialBackOff());
    }

    public void setCredentialAccountName(String accountName){
        mCredential.setSelectedAccountName(accountName);
    }

    public GoogleAccountCredential getCredentialAccountName() {
        return mCredential;
    }

Z poziomu Activity po utworzeniu obiektu wykonywana jest metoda getAccountCallGoogleCalendarApi() uruchamiająca np. wybór konta Google’a czy też dedykowane API np. kalendarza Google’a.

   /**
     * Attempt to call the API, after verifying that all the preconditions are
     * satisfied. The preconditions are: Google Play Services installed, an
     * account was selected and the device currently has online access. If any
     * of the preconditions are not satisfied, the app will prompt the user as
     * appropriate.
     */
    public void getAccountCallGoogleCalendarApi(ApiGoogleCalendarOperation operation, String... params) {
        getAccountCallGoogleCalendarApi(false, operation, params);
    }

    public void getAccountCallGoogleCalendarApi(boolean askAboutAccountName, ApiGoogleCalendarOperation operation, String... params) {
        if (! isGooglePlayServicesAvailable()) {
            // no Google services
            acquireGooglePlayServices();
        } else if (mCredential.getSelectedAccountName() == null) {
            // no Google account selected
            chooseAccount(askAboutAccountName, operation);
        } else if (! isDeviceOnline()) {
            // device is offline
            // print information in UI
        } else {
            // execute request class with credentials
            new ApiGoogleCalendar(mContext, mActivity, this, mCredential, operation).execute(params);
        }
    }

Metody wyboru konta, sprawdzania dostępności sieci Internet czy też usług Google Play Services są analogiczne jak w przewodniku Google’a.

    /**
     * Attempts to set the account used with the API credentials. If an account
     * name was previously saved it will use that one; otherwise an account
     * picker dialog will be shown to the user. Note that the setting the
     * account to use with the credentials object requires the app to have the
     * GET_ACCOUNTS permission, which is requested here if it is not already
     * present. The AfterPermissionGranted annotation indicates that this
     * function will be rerun automatically whenever the GET_ACCOUNTS permission
     * is granted.
     */
    @AfterPermissionGranted(REQUEST_PERMISSION_GET_ACCOUNTS)
    public void chooseAccount(boolean askAboutAccountName, ApiGoogleCalendarOperation operation, String... params) {
        if (EasyPermissions.hasPermissions(mContext, Manifest.permission.GET_ACCOUNTS)) {
            // try to get from preferences account
            String accountName = PreferenceManager.getDefaultSharedPreferences(mContext).getString(ApiGoogle.PREF_GOOGLE_ACCOUNT, null);

            if (askAboutAccountName) {
                // start a dialog from which the user can choose an account
                mActivity.startActivityForResult(
                        mCredential.newChooseAccountIntent(),
                        REQUEST_ACCOUNT_PICKER);
            }
            else if (accountName != null) {
                // success loading account name from preferences
                mCredential.setSelectedAccountName(accountName);
                getAccountCallGoogleCalendarApi(false, operation, params);
            } else {
                // no account and no possibility to choose account via dialog
            }
        } else {
            // request the GET_ACCOUNTS permission via a user dialog
            EasyPermissions.requestPermissions(
                    this,
                    "This app needs to access your Google account (via Contacts).",
                    REQUEST_PERMISSION_GET_ACCOUNTS,
                    Manifest.permission.GET_ACCOUNTS);
        }
    }

    /**
     * Checks whether the device currently has a network connection.
     * @return true if the device has a network connection, false otherwise.
     */
    private boolean isDeviceOnline() {
        ConnectivityManager connMgr =
                (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    /**
     * Check that Google Play services APK is installed and up to date.
     * @return true if Google Play Services is available and up to
     *     date on this device; false otherwise.
     */
    private boolean isGooglePlayServicesAvailable() {
        GoogleApiAvailability apiAvailability =
                GoogleApiAvailability.getInstance();
        final int connectionStatusCode =
                apiAvailability.isGooglePlayServicesAvailable(mContext);
        return connectionStatusCode == ConnectionResult.SUCCESS;
    }

    /**
     * Attempt to resolve a missing, out-of-date, invalid or disabled Google
     * Play Services installation via a user dialog, if possible.
     */
    private void acquireGooglePlayServices() {
        GoogleApiAvailability apiAvailability =
                GoogleApiAvailability.getInstance();
        final int connectionStatusCode =
                apiAvailability.isGooglePlayServicesAvailable(mContext);
        if (apiAvailability.isUserResolvableError(connectionStatusCode)) {
            showGooglePlayServicesAvailabilityErrorDialog(connectionStatusCode);
        }
    }


    /**
     * Display an error dialog showing that Google Play Services is missing
     * or out of date.
     * @param connectionStatusCode code describing the presence (or lack of)
     *     Google Play Services on this device.
     */
    void showGooglePlayServicesAvailabilityErrorDialog(
            final int connectionStatusCode) {
        GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance();
        Dialog dialog = apiAvailability.getErrorDialog(
                mActivity,
                connectionStatusCode,
                REQUEST_GOOGLE_PLAY_SERVICES);
        dialog.show();
    }

    /**
     * Callback for when a permission is granted using the EasyPermissions
     * library.
     * @param requestCode The request code associated with the requested
     *         permission
     * @param perms The requested permission list. Never null.
     */
    @Override
    public void onPermissionsGranted(int requestCode, List<String> perms) {
        // Do nothing.
    }

    /**
     * Callback for when a permission is denied using the EasyPermissions
     * library.
     * @param requestCode The request code associated with the requested
     *         permission
     * @param perms The requested permission list. Never null.
     */
    @Override
    public void onPermissionsDenied(int requestCode, List<String> perms) {
        // Do nothing.
    }

    /**
     * Respond to requests for permissions at runtime for API 23 and above.
     * @param requestCode The request code passed in
     *     requestPermissions(android.app.Activity, String, int, String[])
     * @param permissions The requested permissions. Never null.
     * @param grantResults The grant results for the corresponding permissions
     *     which is either PERMISSION_GRANTED or PERMISSION_DENIED. Never null.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        EasyPermissions.onRequestPermissionsResult(
                requestCode, permissions, grantResults, this);
    }

}

Lista możliwych w aplikacji rodzajów żądań API kalendarza Google’a

Na potrzeby API kalendarza zdefiniowany kilka operacji realizowanych przez klasę wywołującą API kalendarza Google’a.

public enum ApiGoogleCalendarOperation {
    GOOGLE_CALENDARS_LIST,
    GOOGLE_EVENTS_ADD,
    GOOGLE_EVENTS_LIST,
    GOOGLE_EVENTS_DELETE,
    GOOGLE_EVENTS_UPDATE
}

Klasa wywołująca API kalendarza Google’a

Operacje realizowane w ramach wywoływania API kalendarza są realizowane w oddzielnym wątku (klasa rozszerza AsyncTask) i komunikacja z API nie jest realizowana w głównym wątku wyświetlającym interfejs użytkownika. Podobnie jak w poprzedniej klasie do konstruktora przekazywana jest Activity oraz Context. Ponadto przekazywany jest obiekt klasy przygotowującej do wywołań API oraz typ operacji zdefiniowanych jako enum.

/**
 * An asynchronous task that handles the Google Calendar API call.
 * Placing the API calls in their own task ensures the UI stays responsive.
 *
 * Created by seba on 27.08.2016.
 */

public class ApiGoogleCalendar extends AsyncTask<String, Integer, List<String[]>> {
    private com.google.api.services.calendar.Calendar mService = null;
    private Exception mLastError = null;

    Context mContext;
    Activity mActivity;
    ApiGoogle mApiGoogle;
    ApiGoogleCalendarOperation mOperation;

    public ApiGoogleCalendar(Context context, Activity activity, ApiGoogle apiGoogle, GoogleAccountCredential credential, ApiGoogleCalendarOperation operation) {
        mContext = context;
        mActivity = activity;
        mApiGoogle = apiGoogle;
        mOperation = operation;
        HttpTransport transport = AndroidHttp.newCompatibleTransport();
        JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();
        mService = new com.google.api.services.calendar.Calendar.Builder(
                transport, jsonFactory, credential)
                .setApplicationName("RecoAlarm")
                .build();
    }

W głównej metodzie doInBackground(), która zawiera logikę realizowaną w oddzielnym wątku, w zależności od typu operacji przekazanej w konstruktorze wywoływane są odpowiednie metody klasy.

    /**
     * Background task to call Google Calendar API.
     * @param params no parameters needed for this task.
     */
    @Override
    protected List<String[]> doInBackground(String... params) {
        try {
            if (mOperation == ApiGoogleCalendarOperation.GOOGLE_CALENDARS_LIST) {
                return getListOfCalendarsNames();
            }
            else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_ADD) {
                return addEventToCalendar(params[0], params[1], params[2], params[3], params[4]);
            }
            else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_DELETE) {
                return deleteEventFromCalendar(params[0], params[1], params[2], params[3], params[4], params[5]);
            }
            else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_UPDATE) {
                return updateEventInCalendar(params[0], params[1], params[2], params[3], params[4], params[5]);
            }
            else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_LIST) {
                return getListOfEvents(params[0]);
            }
            else {
                return null;
            }
        } catch (Exception e) {
            mLastError = e;
            cancel(true);
            return null;
        }
    }

    @Override
    protected void onPreExecute() {
    }

Po zakończeniu pracy dla niektórych operacji konieczne jest przekazanie wyniku do Activity, która API wywołała.

    @Override
    protected void onPostExecute(List<String[]> output) {
        if (mOperation == ApiGoogleCalendarOperation.GOOGLE_CALENDARS_LIST) {
            if (output != null || output.size() > 0) {
                // update Activity, which started Google API, with output
                // ...
                // ...
            }
        }
        else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_ADD) {
            // no results from operation
        }
        else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_DELETE) {
            // no results from operation
        }
        else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_UPDATE) {
            // no results from operation
        }
        else if (mOperation == ApiGoogleCalendarOperation.GOOGLE_EVENTS_LIST) {
            if (output != null || output.size() > 0) {
                // update Activity, which started Google API, with output
                // ...
                // ...
            }
        }
        else {
            // no results from operation
        }
    }

    @Override
    protected void onCancelled() {
        if (mLastError != null) {
            if (mLastError instanceof GooglePlayServicesAvailabilityIOException) {
                mApiGoogle.showGooglePlayServicesAvailabilityErrorDialog(
                        ((GooglePlayServicesAvailabilityIOException) mLastError)
                                .getConnectionStatusCode());
            } else if (mLastError instanceof UserRecoverableAuthIOException) {
                mActivity.startActivityForResult(
                        ((UserRecoverableAuthIOException) mLastError).getIntent(),
                        ApiGoogle.REQUEST_AUTHORIZATION);
            }
        }
    }

Zadania realizowane dla każdego z typów operacji w API kalendarza i wywoływane w metodzie doInBackground() zdefiniowano jako odrębne metody wyspecyfikowane poniżej.

Pobranie listy kalendarzy

    /**
     * Get list of all calendars
     * @return
     */
    private List<String[]> getListOfCalendarsNames() throws IOException {
        List<String[]> calendars = new ArrayList<String[]>();

        String pageToken = null;
        do {
            CalendarList calendarList = mService.calendarList().list().setPageToken(pageToken).execute();
            List<CalendarListEntry> items = calendarList.getItems();

            for (CalendarListEntry calendarListEntry : items) {
                String[] calendarIdName = new String[2];
                calendarIdName[0] = calendarListEntry.getSummary();
                calendarIdName[1] = calendarListEntry.getId();
                calendars.add(calendarIdName);
            }

            pageToken = calendarList.getNextPageToken();
        } while (pageToken != null);

        return calendars;
    }

Pobranie listy zdarzeń

    /**
     * Get list of events
     * @return
     */
    private List<String[]> getListOfEvents(String calendar) throws IOException {
        List<String[]> list = new ArrayList<String[]>();;

        if (calendar != null && calendar.length() > 0) {
            String pageToken = null;
            do {
                Events events = mService.events().list(calendar).setPageToken(pageToken).execute();
                List<Event> items = events.getItems();
                for (Event item : items) {
                    String[] data = new String[5];
                    data[0] = item.getId();
                    data[1] = item.getSummary();
                    data[2] = (item.getDescription() != null && item.getDescription().length() > 0 ? item.getDescription() : "");
                    data[3] = String.valueOf(item.getStart().getDateTime().getValue());
                    data[4] = String.valueOf(item.getEnd().getDateTime().getValue());
                    list.add(data);
                }
                pageToken = events.getNextPageToken();
            } while (pageToken != null);
        }

        return list;
    }

Dodanie zdarzenia do kalendarza

    /**
     * Add event to the calendar
     * @param calendar
     * @param name
     * @param description
     * @param dateStart
     * @param dateStop
     * @return
     */
    private List<String[]> addEventToCalendar(String calendar, String name, String description,
                                            String dateStart, String dateStop) {
        if (calendar != null && calendar.length() > 0
                && name != null && name.length() > 0
                && dateStart != null && dateStart.length() > 0
                && dateStop != null && dateStop.length() > 0) {
            if (description == null) {
                description = "";
            }

            Event event = new Event()
                    .setSummary(name)
                    .setDescription(description);

            EventDateTime start = new EventDateTime()
                    .setDateTime(new DateTime(Utils.dateFromDatetime(dateStart)));
            event.setStart(start);

            EventDateTime end = new EventDateTime()
                    .setDateTime(new DateTime(Utils.dateFromDatetime(dateStop)));
            event.setEnd(end);

            try {
                event = mService.events().insert(calendar, event).execute();
            } catch (IOException e) {
            }
        }

        return null;
    }

Aktualizacja zdarzenia w kalendarzu

    /**
     * Update event in calendar
     * @param calendar
     * @param eventId
     * @param name
     * @param description
     * @param dateStart
     * @param dateStop
     * @return
     */
    private List<String[]> updateEventInCalendar(String calendar, String eventId, String name, String description, String dateStart, String dateStop) {
        if (calendar != null && calendar.length() > 0
                && name != null && name.length() > 0
                && dateStart != null && dateStart.length() > 0
                && dateStop != null && dateStop.length() > 0
                && eventId != null && eventId.length() > 0) {
            if (description == null) {
                description = "";
            }

            Event event = new Event()
                    .setSummary(name)
                    .setDescription(description);

            EventDateTime start = new EventDateTime()
                    .setDateTime(new DateTime(Utils.dateFromDatetime(dateStart)));
            event.setStart(start);

            EventDateTime end = new EventDateTime()
                    .setDateTime(new DateTime(Utils.dateFromDatetime(dateStop)));
            event.setEnd(end);

            try {
                mService.events().update(calendar, eventId, event).execute();
            } catch (IOException e) {
            }
        }

        return null;
    }

Usunięcie zdarzenia z kalendarza

    /**
     * Delete event from calendar
     * @param calendar
     * @param name
     * @param description
     * @param dateStart
     * @param dateStop
     * @return
     */
    private List<String[]> deleteEventFromCalendar(String calendar, String eventId, String name, String description, String dateStart, String dateStop) {
        if (calendar != null && calendar.length() > 0
                && eventId != null && eventId.length() > 0) {
            try {
                mService.events().delete(calendar, eventId).execute();
            } catch (IOException e) {
            }
        }

        return null;
    }

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Time limit is exhausted. Please reload CAPTCHA.