io.github.hidroh.materialistic.data.ItemSyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for io.github.hidroh.materialistic.data.ItemSyncAdapter.java

Source

/*
 * Copyright (c) 2016 Ha Duy Trung
 *
 * 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 io.github.hidroh.materialistic.data;

import android.accounts.Account;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Process;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;

import io.github.hidroh.materialistic.AppUtils;
import io.github.hidroh.materialistic.Application;
import io.github.hidroh.materialistic.ItemActivity;
import io.github.hidroh.materialistic.Preferences;
import io.github.hidroh.materialistic.R;
import io.github.hidroh.materialistic.annotation.Synthetic;
import retrofit2.Call;
import retrofit2.Callback;

/**
 * Simple sync adapter that triggers OkHttp requests so their responses become available in
 * cache for subsequent requests
 */
class ItemSyncAdapter extends AbstractThreadedSyncAdapter {

    static final String SYNC_PREFERENCES_FILE = "_syncpreferences";
    private static final String EXTRA_ID = ItemSyncAdapter.class.getName() + ".EXTRA_ID";
    private static final String NOTIFICATION_GROUP_KEY = "group";
    private static final String EXTRA_CONNECTION_ENABLED = ItemSyncAdapter.class.getName()
            + ".EXTRA_CONNECTION_ENABLED";
    private static final String EXTRA_READABILITY_ENABLED = ItemSyncAdapter.class.getName()
            + ".EXTRA_READABILITY_ENABLED";
    private static final String EXTRA_COMMENTS_ENABLED = ItemSyncAdapter.class.getName()
            + ".EXTRA_COMMENTS_ENABLED";
    private static final String EXTRA_NOTIFICATION_ENABLED = ItemSyncAdapter.class.getName()
            + ".EXTRA_NOTIFICATION_ENABLED";

    @UiThread
    static void initSync(Context context, @Nullable String itemId) {
        if (!Preferences.Offline.isEnabled(context)) {
            return;
        }
        Bundle extras = new Bundle();
        if (itemId != null) {
            extras.putString(ItemSyncAdapter.EXTRA_ID, itemId);
        }
        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        extras.putBoolean(ItemSyncAdapter.EXTRA_CONNECTION_ENABLED,
                Preferences.Offline.currentConnectionEnabled(context));
        extras.putBoolean(ItemSyncAdapter.EXTRA_READABILITY_ENABLED,
                Preferences.Offline.isReadabilityEnabled(context));
        extras.putBoolean(ItemSyncAdapter.EXTRA_COMMENTS_ENABLED, Preferences.Offline.isCommentsEnabled(context));
        extras.putBoolean(ItemSyncAdapter.EXTRA_NOTIFICATION_ENABLED,
                Preferences.Offline.isNotificationEnabled(context));
        ContentResolver.requestSync(Application.createSyncAccount(), MaterialisticProvider.PROVIDER_AUTHORITY,
                extras);
    }

    private final HackerNewsClient.RestService mHnRestService;
    private final ReadabilityClient mReadabilityClient;
    private final SharedPreferences mSharedPreferences;
    private final NotificationManager mNotificationManager;
    private final NotificationCompat.Builder mNotificationBuilder;
    private final Map<String, SyncProgress> mSyncProgresses = new HashMap<>();
    private boolean mConnectionEnabled;
    private boolean mReadabilityEnabled;
    private boolean mCommentsEnabled;
    private boolean mNotificationEnabled;

    ItemSyncAdapter(Context context, RestServiceFactory factory, ReadabilityClient readabilityClient) {
        super(context, true);
        mSharedPreferences = context.getSharedPreferences(context.getPackageName() + SYNC_PREFERENCES_FILE,
                Context.MODE_PRIVATE);
        mHnRestService = factory.create(HackerNewsClient.BASE_API_URL, HackerNewsClient.RestService.class,
                new BackgroundThreadExecutor());
        mReadabilityClient = readabilityClient;
        mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationBuilder = new NotificationCompat.Builder(getContext())
                .setLargeIcon(BitmapFactory.decodeResource(getContext().getResources(), R.mipmap.ic_launcher))
                .setSmallIcon(R.drawable.ic_notification).setGroup(NOTIFICATION_GROUP_KEY)
                .setCategory(NotificationCompat.CATEGORY_PROGRESS).setAutoCancel(true);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
            SyncResult syncResult) {
        // assume that connection wouldn't change until we finish syncing
        mConnectionEnabled = extras.getBoolean(EXTRA_CONNECTION_ENABLED);
        mReadabilityEnabled = extras.getBoolean(EXTRA_READABILITY_ENABLED);
        mCommentsEnabled = extras.getBoolean(EXTRA_COMMENTS_ENABLED);
        mNotificationEnabled = extras.getBoolean(EXTRA_NOTIFICATION_ENABLED);
        if (extras.containsKey(EXTRA_ID)) {
            String id = extras.getString(EXTRA_ID);
            mSyncProgresses.put(id, new SyncProgress(id));
            sync(id, id);
        } else {
            syncDeferredItems();
        }
    }

    private void syncDeferredItems() {
        Set<String> itemIds = mSharedPreferences.getAll().keySet();
        for (String itemId : itemIds) {
            sync(itemId, null); // do not show notifications for deferred items
        }
    }

    private void sync(String itemId, final String progressId) {
        if (!mConnectionEnabled) {
            defer(itemId);
            return;
        }
        HackerNewsItem cachedItem;
        if ((cachedItem = getFromCache(itemId)) != null) {
            sync(cachedItem, progressId);
        } else {
            showNotification(progressId);
            // TODO defer on low battery as well?
            mHnRestService.networkItem(itemId).enqueue(new Callback<HackerNewsItem>() {
                @Override
                public void onResponse(Call<HackerNewsItem> call, retrofit2.Response<HackerNewsItem> response) {
                    HackerNewsItem item;
                    if ((item = response.body()) != null) {
                        sync(item, progressId);
                    }
                }

                @Override
                public void onFailure(Call<HackerNewsItem> call, Throwable t) {
                    notifyItem(progressId, itemId, null);
                }
            });
        }
    }

    @Synthetic
    void sync(@NonNull HackerNewsItem item, String progressId) {
        mSharedPreferences.edit().remove(item.getId()).apply();
        notifyItem(progressId, item.getId(), item);
        syncReadability(item);
        syncArticle(item);
        syncChildren(item);
    }

    private void syncReadability(@NonNull HackerNewsItem item) {
        if (mReadabilityEnabled && item.isStoryType()) {
            final String itemId = item.getId();
            mReadabilityClient.parse(itemId, item.getRawUrl());
            notifyReadability(itemId);
        }
    }

    private void syncArticle(@NonNull HackerNewsItem item) {
        if (item.isStoryType()) {
            ItemSyncService.WebCacheReceiver.initSave(getContext(), item.getUrl());
        }
    }

    private void syncChildren(@NonNull HackerNewsItem item) {
        if (mCommentsEnabled && item.getKids() != null) {
            for (long id : item.getKids()) {
                sync(String.valueOf(id), item.getId());
            }
        }
    }

    private void defer(String itemId) {
        mSharedPreferences.edit().putBoolean(itemId, true).apply();
    }

    private HackerNewsItem getFromCache(String itemId) {
        try {
            return mHnRestService.cachedItem(itemId).execute().body();
        } catch (IOException e) {
            return null;
        }
    }

    private boolean isNotificationEnabled(@Nullable String progressId) {
        return mNotificationEnabled && progressId != null && mSyncProgresses.containsKey(progressId);
    }

    @Synthetic
    void notifyItem(@Nullable String progressId, @NonNull String id, @Nullable HackerNewsItem item) {
        if (isNotificationEnabled(progressId)) {
            mSyncProgresses.get(progressId).finishItem(id, item, mCommentsEnabled && mConnectionEnabled,
                    mReadabilityEnabled && mConnectionEnabled);
            showNotification(progressId);
        }
    }

    private void notifyReadability(@Nullable String progressId) {
        if (isNotificationEnabled(progressId)) {
            mSyncProgresses.get(progressId).finishReadability();
            showNotification(progressId);
        }
    }

    private void showNotification(String progressId) {
        if (!isNotificationEnabled(progressId)) {
            return;
        }
        SyncProgress syncProgress = mSyncProgresses.get(progressId);
        if (syncProgress.getProgress() >= syncProgress.getMax()) {
            mSyncProgresses.remove(progressId);
            mNotificationManager.cancel(Integer.valueOf(progressId));
        } else {
            mNotificationManager.notify(Integer.valueOf(progressId),
                    mNotificationBuilder.setContentTitle(getContext().getString(R.string.download_in_progress))
                            .setContentText(syncProgress.title).setContentIntent(getItemActivity(progressId))
                            .setProgress(syncProgress.getMax(), syncProgress.getProgress(), false)
                            .setSortKey(progressId).build());
        }
    }

    private PendingIntent getItemActivity(String itemId) {
        return PendingIntent.getActivity(getContext(), 0,
                new Intent(Intent.ACTION_VIEW).setData(AppUtils.createItemUri(itemId))
                        .putExtra(ItemActivity.EXTRA_CACHE_MODE, ItemManager.MODE_CACHE)
                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
                PendingIntent.FLAG_ONE_SHOT);
    }

    static class SyncProgress {
        private final String id;
        private Boolean self;
        private int totalKids, finishedKids;
        private Boolean readability;
        String title;

        @Synthetic
        SyncProgress(String id) {
            this.id = id;
        }

        int getMax() {
            return 1 + totalKids + (readability != null ? 1 : 0);
        }

        int getProgress() {
            return (self != null ? 1 : 0) + finishedKids + (readability != null && readability ? 1 : 0);
        }

        @Synthetic
        void finishItem(@NonNull String id, @Nullable HackerNewsItem item, boolean kidsEnabled,
                boolean readabilityEnabled) {
            if (TextUtils.equals(id, this.id)) {
                finishSelf(item, kidsEnabled, readabilityEnabled);
            } else {
                finishKid();
            }
        }

        @Synthetic
        void finishReadability() {
            readability = true;
        }

        private void finishSelf(@Nullable HackerNewsItem item, boolean kidsEnabled, boolean readabilityEnabled) {
            self = item != null;
            title = item != null ? item.getTitle() : null;
            if (kidsEnabled && item != null && item.getKids() != null) {
                // fetch recursively but only notify for immediate children
                totalKids = item.getKids().length;
            } else {
                totalKids = 0;
            }
            if (readabilityEnabled) {
                readability = false;
            }
        }

        private void finishKid() {
            finishedKids++;
        }
    }

    static class BackgroundThreadExecutor implements Executor {

        @Override
        public void execute(@NonNull Runnable r) {
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
            r.run();
        }
    }
}