com.actionlauncher.api.LiveWallpaperSource.java Source code

Java tutorial

Introduction

Here is the source code for com.actionlauncher.api.LiveWallpaperSource.java

Source

/*
 * Copyright 2015 Chris Lacy
 *
 * 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.actionlauncher.api;

import android.app.IntentService;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;

import com.actionlauncher.api.actionpalette.ActionPalette;
import com.actionlauncher.api.internal.ProtocolConstants;
import com.actionlauncher.api.internal.SourceState;

import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static com.actionlauncher.api.internal.ProtocolConstants.ACTION_FETCH_PALETTE;
import static com.actionlauncher.api.internal.ProtocolConstants.ACTION_PUBLISH_STATE;
import static com.actionlauncher.api.internal.ProtocolConstants.ACTION_SUBSCRIBE;
import static com.actionlauncher.api.internal.ProtocolConstants.EXTRA_LIVE_WALLPAPER_INFO;
import static com.actionlauncher.api.internal.ProtocolConstants.EXTRA_STATE;
import static com.actionlauncher.api.internal.ProtocolConstants.EXTRA_SUBSCRIBER_COMPONENT;
import static com.actionlauncher.api.internal.ProtocolConstants.EXTRA_TOKEN;

/**
 *
 */
public class LiveWallpaperSource extends IntentService {
    private static final String TAG = "Action3-api";
    private static boolean LOGGING_ENABLED = false;

    /**
     * The {@link Intent} action representing an Action Launcher live wallpaper source. This service
     * should declare an <code>&lt;intent-filter&gt;</code> for this action in order to register with
     * Action Launcher 3.
     */
    public static final String ACTION_WALLPAPER_SOURCE = "com.actionlauncher.api.action.LiveWallpaperSource";

    private static final String PREF_STATE = "state";
    private static final String PREF_SUBSCRIPTIONS = "subscriptions";

    private static final int MSG_PUBLISH_CURRENT_STATE = 1;

    private SharedPreferences mSharedPrefs;

    private String mName = "<not_set>";

    private Map<ComponentName, String> mSubscriptions;
    private SourceState mCurrentState;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MSG_PUBLISH_CURRENT_STATE) {
                publishCurrentState();
                saveState();
            }
        }
    };

    public LiveWallpaperSource() {
        this("<not_set>"); // mName is set with the package name in onCreate()
    }

    /**
     * Remember to call this constructor from an empty constructor!
     *
     * @param name Should be an ID-style name for your source, usually just the class name. This is
     *             not user-visible and is only used for {@linkplain #getSharedPreferences()
     *             storing preferences} and in system log output.
     */
    public LiveWallpaperSource(String name) {
        super(name);
        mName = name;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        mSharedPrefs = getSharedPreferences();
        loadSubscriptions();
        loadState();
    }

    /**
     * Method called before a new subscriber is added that determines whether the subscription is
     * allowed or not. The default behavior is to allow all subscriptions.
     *
     * @return true if the subscription should be allowed, false if it should be denied.
     */
    protected boolean onAllowSubscription(ComponentName subscriber) {
        return true;
    }

    /**
     * Lifecycle method called when a new subscriber is added. Sources generally don't need to
     * override this. For more details on the source lifecycle, see the discussion in the
     * {@link LiveWallpaperSource} reference.
     */
    protected void onSubscriberAdded(ComponentName subscriber) {
    }

    /**
     * Lifecycle method called when a subscriber is removed. Sources generally don't need to
     * override this. For more details on the source lifecycle, see the discussion in the
     * {@link LiveWallpaperSource} reference.
     */
    protected void onSubscriberRemoved(ComponentName subscriber) {
    }

    /**
     * Lifecycle method called when the first subscriber is added. This will be called before
     * {@link #onSubscriberAdded(ComponentName)}. Sources generally don't need to override this.
     * For more details on the source lifecycle, see the discussion in the {@link LiveWallpaperSource}
     * reference.
     */
    protected void onEnabled() {
    }

    /**
     * Lifecycle method called when the last subscriber is removed. This will be called after
     * {@link #onSubscriberRemoved(ComponentName)}. Sources generally don't need to override this.
     * For more details on the source lifecycle, see the discussion in the {@link LiveWallpaperSource}
     * reference.
     */
    protected void onDisabled() {
    }

    /**
     * Publishes the provided {@link LiveWallpaperInfo} object. This will be sent to all current subscribers
     * and to all future subscribers, until a new item is published.
     *
     * @param liveWallpaperInfo the LiveWallpaperInfo to publish
     */
    protected final void publishLiveWallpaperInfo(LiveWallpaperInfo liveWallpaperInfo) {
        mCurrentState.setCurrentLiveWallpaperInfo(liveWallpaperInfo);
        mHandler.removeMessages(MSG_PUBLISH_CURRENT_STATE);
        mHandler.sendEmptyMessage(MSG_PUBLISH_CURRENT_STATE);
    }

    /**
     * Returns the most recently {@linkplain #publishLiveWallpaperInfo(LiveWallpaperInfo) published} item, or null
     * if none has been published.
     *
     * @return the current LiveWallpaperInfo (if one exists).
     */
    protected final LiveWallpaperInfo getCurrentLiveWallpaperInfo() {
        return mCurrentState != null ? mCurrentState.getCurrentLiveWallpaperInfo() : null;
    }

    /**
     * Returns true if this source is enabled; that is, if there is at least one active subscriber.
     *
     * @see #onEnabled()
     * @see #onDisabled()
     *
     * @return true if enabled.
     */
    protected synchronized final boolean isEnabled() {
        return mSubscriptions.size() > 0;
    }

    /**
     * Convenience method for accessing preferences specific to the source (with the given name
     * within this package. The source name must be the one provided in the
     * {@link #LiveWallpaperSource(String)} constructor. This static method is useful for exposing source
     * preferences to other application components such as the source settings activity.
     *
     * @param context    The context; can be an application context.
     * @param sourceName The source name, provided in the {@link #LiveWallpaperSource(String)}
     *                   constructor.
     */
    protected static SharedPreferences getSharedPreferences(Context context, String sourceName) {
        return context.getSharedPreferences("action3source_" + sourceName, 0);
    }

    /**
     * Convenience method for accessing preferences specific to the source.
     */
    protected final SharedPreferences getSharedPreferences() {
        return getSharedPreferences(this, mName);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (intent == null) {
            return;
        }

        String action = intent.getAction();
        LOGD("LiveWallpaperSource.onHandleIntent() - action:" + action + ", id:" + mName);
        // TODO: permissions?
        if (ACTION_SUBSCRIBE.equals(action)) {
            processSubscribe((ComponentName) intent.getParcelableExtra(EXTRA_SUBSCRIBER_COMPONENT),
                    intent.getStringExtra(EXTRA_TOKEN));
        } else if (ACTION_FETCH_PALETTE.equals(action)) {
            publishCurrentPalette();
        } else if (action.equals(ProtocolConstants.ACTION_PUBLISH_STATE)) {
            boolean wallpaperInfoSet = false;
            if (intent.hasExtra(EXTRA_LIVE_WALLPAPER_INFO)) {
                Bundle bundle = intent.getExtras().getBundle(EXTRA_LIVE_WALLPAPER_INFO);
                if (bundle != null) {
                    LiveWallpaperInfo info = LiveWallpaperInfo.fromBundle(bundle);
                    mCurrentState.setCurrentLiveWallpaperInfo(info);
                    LOGD("LiveWallpaperInfo.fromBundle():" + (info != null ? info.toString() : null));
                    wallpaperInfoSet = true;
                }
            }
            if (!wallpaperInfoSet) {
                mCurrentState.setCurrentLiveWallpaperInfo(null);
            }
            publishCurrentPalette();
        }
    }

    public void publishCurrentPalette() {
        LOGD("publishCurrentPalette()");
        mHandler.removeMessages(MSG_PUBLISH_CURRENT_STATE);
        mHandler.sendEmptyMessage(MSG_PUBLISH_CURRENT_STATE);
    }

    private synchronized void processSubscribe(ComponentName subscriber, String token) {
        if (subscriber == null) {
            LOGD("No subscriber given.");
            return;
        }

        String oldToken = mSubscriptions.get(subscriber);
        if (TextUtils.isEmpty(token)) {
            if (oldToken == null) {
                return;
            }

            // Unsubscribing
            mSubscriptions.remove(subscriber);
            processAndDispatchSubscriberRemoved(subscriber);

        } else {
            // Subscribing
            if (!TextUtils.isEmpty(oldToken)) {
                // Was previously subscribed, treat this as a unsubscribe + subscribe
                mSubscriptions.remove(subscriber);
                processAndDispatchSubscriberRemoved(subscriber);
            }

            if (!onAllowSubscription(subscriber)) {
                return;
            }

            mSubscriptions.put(subscriber, token);
            processAndDispatchSubscriberAdded(subscriber);
        }

        saveSubscriptions();
    }

    private synchronized void processAndDispatchSubscriberAdded(ComponentName subscriber) {
        // Trigger callbacks
        if (mSubscriptions.size() == 1) {
            onEnabled();
        }

        onSubscriberAdded(subscriber);

        LOGD("processAndDispatchSubscriberAdded():" + subscriber + ", mSubscriptions.size():"
                + mSubscriptions.size());

        // If there's no LiveWallpaperInfo, trigger initial update
        //if (mSubscriptions.size() == 1
        //        && mLiveWallpaperInfo == null) {
        //    // TODO: Broadcast that we need a palette
        //}

        // Immediately publish current state to subscriber
        publishCurrentState(subscriber);
    }

    private synchronized void processAndDispatchSubscriberRemoved(ComponentName subscriber) {
        // Trigger callbacks
        onSubscriberRemoved(subscriber);
        if (mSubscriptions.size() == 0) {
            onDisabled();
        }
        LOGD("processAndDispatchSubscriberRemoved():" + subscriber + ", mSubscriptions.size():"
                + mSubscriptions.size());
    }

    private synchronized void publishCurrentState() {
        for (ComponentName subscription : mSubscriptions.keySet()) {
            publishCurrentState(subscription);
        }
    }

    private synchronized void publishCurrentState(final ComponentName subscriber) {
        String token = mSubscriptions.get(subscriber);
        if (TextUtils.isEmpty(token)) {
            LOGD("Not active, canceling update, id=" + mName);
            return;
        }

        // Publish update
        Intent intent = new Intent(ACTION_PUBLISH_STATE).setComponent(subscriber).putExtra(EXTRA_TOKEN, token)
                .putExtra(EXTRA_STATE, (mCurrentState != null) ? mCurrentState.toBundle() : null);
        try {
            ComponentName returnedSubscriber = startService(intent);
            if (returnedSubscriber == null) {
                LOGE("Update wasn't published because subscriber no longer exists" + ", id=" + mName);
                // Unsubscribe the now-defunct subscriber
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processSubscribe(subscriber, null);
                    }
                });
            } else {
                LOGD("publishCurrentState(): successfully started service " + returnedSubscriber.toString()
                        + " with intent " + intent.toString());
            }
        } catch (SecurityException e) {
            LOGE("Couldn't publish update, id=" + mName, e);
        }
    }

    private synchronized void loadSubscriptions() {
        mSubscriptions = new HashMap<ComponentName, String>();
        Set<String> serializedSubscriptions = mSharedPrefs.getStringSet(PREF_SUBSCRIPTIONS, null);
        if (serializedSubscriptions != null) {
            for (String serializedSubscription : serializedSubscriptions) {
                String[] arr = serializedSubscription.split("\\|", 2);
                ComponentName subscriber = ComponentName.unflattenFromString(arr[0]);
                String token = arr[1];
                mSubscriptions.put(subscriber, token);
            }
        }
    }

    private synchronized void saveSubscriptions() {
        Set<String> serializedSubscriptions = new HashSet<String>();
        for (ComponentName subscriber : mSubscriptions.keySet()) {
            serializedSubscriptions.add(subscriber.flattenToShortString() + "|" + mSubscriptions.get(subscriber));
        }
        mSharedPrefs.edit().putStringSet(PREF_SUBSCRIPTIONS, serializedSubscriptions).commit();
    }

    private void loadState() {
        String stateString = mSharedPrefs.getString(PREF_STATE, null);
        if (stateString != null) {
            try {
                mCurrentState = SourceState.fromJson((JSONObject) new JSONTokener(stateString).nextValue());
            } catch (JSONException e) {
                LOGE("Couldn't deserialize current state, id=" + mName, e);
            }
        } else {
            mCurrentState = new SourceState();
        }
    }

    private void saveState() {
        try {
            String state = mCurrentState.toJson().toString();
            mSharedPrefs.edit().putString(PREF_STATE, state).commit();
            LOGD("saveState() - " + state);
        } catch (JSONException e) {
            LOGE("Couldn't serialize current state, id=" + mName, e);
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    static void LOGD(String msg) {
        LOGD(msg, null);
    }

    static void LOGD(String msg, Throwable throwable) {
        if (LOGGING_ENABLED) {
            Log.d(TAG, msg, throwable);
        }
    }

    static void LOGE(String msg) {
        LOGE(msg, null);
    }

    static void LOGE(String msg, Throwable throwable) {
        if (LOGGING_ENABLED) {
            Log.e(TAG, msg, throwable);
        }
    }

    /**
     * A <a href="http://en.wikipedia.org/wiki/Builder_pattern">builder</a>-style, <a
     * href="http://en.wikipedia.org/wiki/Fluent_interface">fluent interface</a> for initiating
     * the API.
     *
     * Example of the simplest usage:
     *
     * try {
     *     LiveWallpaperSource.with(mContext)
     *          .loggingEnabled(false)
     *          .setBitmapSynchronous(tempBitmap)
     *          .run();
     * } catch (OutOfMemoryError outOfMemoryError) {
     *    ...
     * } catch (IllegalArgumentException illegalArgumentEx) {
     *    ...
     * } catch (IllegalStateException illegalStateException) {
     *    ...
     * }
     *
     */
    static public class Builder {

        Context mContext;
        ActionPalette mActionPalette;

        Builder(Context context) {
            mContext = context.getApplicationContext();
        }

        /**
         * Set the Bitmap, and generate a palette for the supplied Bitmap.
         * Occurs synchronously, so put inside a thread/AsyncTask.
         *
         * @param bitmap The bitmap to process
         * @return the builder instance
         */
        public Builder setBitmapSynchronous(Bitmap bitmap) {
            mActionPalette = ActionPalette.from(bitmap).generate();
            return this;
        }

        /**
         *
         * @param colors
         * @return
         */
        /*
        public Builder setFallbackColors(Set<Integer> colors) {
        return this;
        }*/

        /**
         * Should Log.d() and Log.e() calls be made. Used for debugging.
         *
         * @param enabled true if logging is to be enabled (uses the "Action3-api" tag)
         * @return
         */
        public Builder loggingEnabled(boolean enabled) {
            LOGGING_ENABLED = enabled;
            return this;
        }

        /**
         * Kick off communication with Action Launcher.
         *
         * @return true if communication with Action Launcher was initiated, false if not.
         */
        public boolean run() {
            Intent serviceIntent = new Intent(mContext, LiveWallpaperSource.class)
                    .setAction(ProtocolConstants.ACTION_PUBLISH_STATE)
                    .putExtra(EXTRA_LIVE_WALLPAPER_INFO,
                            (mActionPalette == null) ? null
                                    : new LiveWallpaperInfo.Builder().palette(mActionPalette).build().toBundle())
                    .putExtra("dummy", System.currentTimeMillis());
            try {
                ComponentName result = mContext.startService(serviceIntent);
                LOGD("startService() result:" + result);
                return result != null;
            } catch (Exception ex) {
                LOGE("Error starting service with intent:" + serviceIntent + "\n" + ex.getLocalizedMessage(), ex);
            }
            return false;
        }
    }

    public static Builder with(Context context) {
        return new Builder(context);
    }

}