blob: fa40888d87bbd20d7e64268104a3fd5efcb99b67 [file] [log] [blame]
// Copyright 2010-2015, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package org.mozc.android.inputmethod.japanese.preference;
import org.mozc.android.inputmethod.japanese.MozcLog;
import org.mozc.android.inputmethod.japanese.MozcUtil;
import org.mozc.android.inputmethod.japanese.keyboard.BackgroundDrawableFactory;
import org.mozc.android.inputmethod.japanese.keyboard.KeyState.MetaState;
import org.mozc.android.inputmethod.japanese.keyboard.Keyboard;
import org.mozc.android.inputmethod.japanese.keyboard.Keyboard.KeyboardSpecification;
import org.mozc.android.inputmethod.japanese.keyboard.KeyboardParser;
import org.mozc.android.inputmethod.japanese.keyboard.KeyboardViewBackgroundSurface;
import org.mozc.android.inputmethod.japanese.preference.ClientSidePreference.KeyboardLayout;
import org.mozc.android.inputmethod.japanese.resources.R;
import org.mozc.android.inputmethod.japanese.view.DrawableCache;
import org.mozc.android.inputmethod.japanese.view.Skin;
import com.google.common.base.Preconditions;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import java.util.WeakHashMap;
import javax.annotation.Nullable;
/**
* A Drawable to render keyboard preview.
*
*/
public class KeyboardPreviewDrawable extends Drawable {
/**
* The key of the activity which uses the Bitmap cache.
*
* In order to utilize memories, it is necessary to tell
* to the BitmapCache what activities use it now.
* The current heuristic is register the activity in its onStart
* and unregister it in its onStop.
* In theory, it should work well as long as the onStop
* corresponding to the already-invoked onStart is invoked without errors.
*
* As the last resort, we use a finalizer guardian.
* The instance of the key should be referred only by an Activity instance
* in its field. Then:
* - the activity can register and unregister in its onStart/onStop methods.
* - Even if onStop is NOT invoked accidentally, the Activity will be
* collected by GC, and the key is also collected at the same time.
* Then the finalize of the key will be invoked, and it will unregister
* itself from Bitmap cache.
* - Overriding the finalizer may cause the delay of memory collecting.
* However, the CacheReferenceKey is small enough (at least compared to
* Activity as the Activity refers many other instances, too). So the
* risk (or damage) of the remaining phantom instances should be low enough.
* Note: Regardless of the finalizer, if onStop is correctly invoked, the bitmap
* cache will be released correctly.
*/
static class CacheReferenceKey {
@Override
protected void finalize() throws Throwable {
BitmapCache.getInstance().removeReference(this);
super.finalize();
}
}
/**
* Global cache of bitmap for the keyboard preview.
*
* Assuming that the size of each bitmap preview is same, and skin type is globally unique,
* we can use global bitmap cache to keep the memory usage low.
* This cache also manages the referencing Activities. See {@link MozcBasePreferenceActivity}
* for the details.
*/
static class BitmapCache {
private static final BitmapCache INSTANCE = new BitmapCache();
private static final Object DUMMY_VALUE = new Object();
private final Map<KeyboardLayout, Bitmap> map =
new EnumMap<KeyboardLayout, Bitmap>(KeyboardLayout.class);
private Skin skin = Skin.getFallbackInstance();
private final WeakHashMap<CacheReferenceKey, Object> referenceMap =
new WeakHashMap<CacheReferenceKey, Object>();
private BitmapCache() {
}
static BitmapCache getInstance() {
return INSTANCE;
}
@Nullable Bitmap get(KeyboardLayout keyboardLayout, int width, int height, Skin skin) {
Preconditions.checkNotNull(skin);
if (keyboardLayout == null || width <= 0 || height <= 0) {
return null;
}
if (!skin.equals(this.skin)) {
return null;
}
Bitmap result = map.get(keyboardLayout);
if (result != null) {
// Check the size.
if (result.getWidth() != width || result.getHeight() != height) {
result = null;
}
}
return result;
}
void put(@Nullable KeyboardLayout keyboardLayout, Skin skin, @Nullable Bitmap bitmap) {
Preconditions.checkNotNull(skin);
if (keyboardLayout == null || bitmap == null) {
return;
}
if (!skin.equals(this.skin)) {
clear();
this.skin = skin;
}
Bitmap oldBitmap = map.put(keyboardLayout, bitmap);
if (oldBitmap != null) {
// Recycle old bitmap if exists.
oldBitmap.recycle();
}
}
void addReference(CacheReferenceKey key) {
referenceMap.put(key, DUMMY_VALUE);
}
void removeReference(CacheReferenceKey key) {
referenceMap.remove(key);
if (referenceMap.isEmpty()) {
// When all referring activities are gone, we don't need keep the cache.
// To reduce the memory usage, release all the cached bitmap.
clear();
}
}
private void clear() {
for (Bitmap bitmap : map.values()) {
if (bitmap != null) {
bitmap.recycle();
}
}
map.clear();
skin = Skin.getFallbackInstance();
}
}
private final Resources resources;
private final KeyboardLayout keyboardLayout;
private final KeyboardSpecification specification;
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private Skin skin = Skin.getFallbackInstance();
private boolean enabled = true;
KeyboardPreviewDrawable(
Resources resources, KeyboardLayout keyboardLayout, KeyboardSpecification specification) {
this.resources = resources;
this.keyboardLayout = keyboardLayout;
this.specification = specification;
}
@Override
public void draw(Canvas canvas) {
Rect bounds = getBounds();
if (bounds.isEmpty()) {
return;
}
// Look up cache.
BitmapCache cache = BitmapCache.getInstance();
Bitmap bitmap = cache.get(keyboardLayout, bounds.width(), bounds.height(), skin);
if (bitmap == null) {
bitmap = createBitmap(
resources, specification, bounds.width(), bounds.height(),
resources.getDimensionPixelSize(R.dimen.pref_inputstyle_reference_width), skin);
if (bitmap != null) {
cache.put(keyboardLayout, skin, bitmap);
}
}
canvas.drawBitmap(bitmap, bounds.left, bounds.top, paint);
if (!enabled) {
// To represent disabling, gray it out.
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0x80000000);
canvas.drawRect(bounds, paint);
}
}
/**
* @param width width of returned {@code Bitmap}
* @param height height of returned {@code Bitmap}
* @param virtualWidth virtual width of keyboard. This value is used when rendering.
* virtualHeight is internally calculated based on given arguments keeping aspect ratio.
*/
@Nullable
private static Bitmap createBitmap(
Resources resources, KeyboardSpecification specification, int width, int height,
int virtualWidth, Skin skin) {
Preconditions.checkNotNull(skin);
// Scaling is required because some icons are draw with specified fixed size.
float scale = width / (float) virtualWidth;
int virtualHeight = (int) (height / scale);
Keyboard keyboard = getParsedKeyboard(resources, specification, virtualWidth, virtualHeight);
if (keyboard == null) {
return null;
}
Bitmap bitmap = MozcUtil.createBitmap(width, height, Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
canvas.scale(scale, scale);
DrawableCache drawableCache = new DrawableCache(resources);
drawableCache.setSkin(skin);
// Fill background.
{
Drawable keyboardBackground =
skin.windowBackgroundDrawable.getConstantState().newDrawable();
keyboardBackground.setBounds(0, 0, virtualWidth, virtualHeight);
keyboardBackground.draw(canvas);
}
// Draw keyboard layout.
{
BackgroundDrawableFactory backgroundDrawableFactory =
new BackgroundDrawableFactory(resources);
backgroundDrawableFactory.setSkin(skin);
KeyboardViewBackgroundSurface backgroundSurface =
new KeyboardViewBackgroundSurface(backgroundDrawableFactory, drawableCache);
backgroundSurface.requestUpdateKeyboard(keyboard, Collections.<MetaState>emptySet());
backgroundSurface.requestUpdateSize(virtualWidth, virtualHeight);
backgroundSurface.update();
backgroundSurface.draw(canvas);
backgroundSurface.reset(); // Release the background bitmap and its canvas.
}
return bitmap;
}
/** Create a Keyboard instance which fits the current bitmap. */
@Nullable
private static Keyboard getParsedKeyboard(
Resources resources, KeyboardSpecification specification, int width, int height) {
KeyboardParser parser = new KeyboardParser(
resources, width, height, specification);
try {
return parser.parseKeyboard();
} catch (XmlPullParserException e) {
MozcLog.e("Failed to parse keyboard layout: ", e);
} catch (IOException e) {
MozcLog.e("Failed to parse keyboard layout: ", e);
}
return null;
}
void setSkin(Skin skin) {
this.skin = skin;
invalidateSelf();
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public void setAlpha(int alpha) {
// Do nothing.
}
@Override
public void setColorFilter(ColorFilter cf) {
// Do nothing.
}
// Hard coded size to adapt the old implementation.
@Override
public int getIntrinsicWidth() {
return resources.getDimensionPixelSize(R.dimen.pref_inputstyle_image_width);
}
@Override
public int getIntrinsicHeight() {
return resources.getDimensionPixelSize(R.dimen.pref_inputstyle_image_width) * 348 / 480;
}
@Override
public boolean isStateful() {
return true;
}
@Override
protected boolean onStateChange(int[] state) {
boolean enabled = isEnabled(state);
if (this.enabled == enabled) {
return false;
}
this.enabled = enabled;
invalidateSelf();
return true;
}
private static boolean isEnabled(int[] state) {
for (int i = 0; i < state.length; ++i) {
if (state[i] == android.R.attr.state_enabled) {
return true;
}
}
return false;
}
}