2009-03-03 19:32:27 -08:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2009 The Android Open Source Project
|
|
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
|
2017-12-18 13:49:44 -08:00
|
|
|
package com.android.launcher3.widget;
|
2009-03-03 19:32:27 -08:00
|
|
|
|
2014-03-05 18:07:04 -08:00
|
|
|
import android.appwidget.AppWidgetProviderInfo;
|
2009-03-03 19:32:27 -08:00
|
|
|
import android.content.Context;
|
2017-10-04 16:50:57 -07:00
|
|
|
import android.content.res.Configuration;
|
2016-12-19 14:12:05 -08:00
|
|
|
import android.graphics.PointF;
|
2020-02-19 08:40:49 -08:00
|
|
|
import android.graphics.Rect;
|
2016-09-25 21:23:25 -07:00
|
|
|
import android.os.Handler;
|
|
|
|
|
import android.os.SystemClock;
|
|
|
|
|
import android.util.SparseBooleanArray;
|
2009-03-03 19:32:27 -08:00
|
|
|
import android.view.LayoutInflater;
|
|
|
|
|
import android.view.MotionEvent;
|
|
|
|
|
import android.view.View;
|
2016-02-09 11:28:52 -08:00
|
|
|
import android.view.ViewDebug;
|
2011-04-13 11:27:36 -07:00
|
|
|
import android.view.ViewGroup;
|
2016-03-10 05:34:30 -08:00
|
|
|
import android.view.accessibility.AccessibilityNodeInfo;
|
2017-01-04 16:31:57 -08:00
|
|
|
import android.widget.AdapterView;
|
2016-09-25 21:23:25 -07:00
|
|
|
import android.widget.Advanceable;
|
2012-06-01 17:17:08 -07:00
|
|
|
import android.widget.RemoteViews;
|
2009-03-03 19:32:27 -08:00
|
|
|
|
2017-12-18 13:49:44 -08:00
|
|
|
import com.android.launcher3.CheckLongPressHelper;
|
|
|
|
|
import com.android.launcher3.ItemInfo;
|
|
|
|
|
import com.android.launcher3.Launcher;
|
|
|
|
|
import com.android.launcher3.LauncherAppWidgetInfo;
|
|
|
|
|
import com.android.launcher3.LauncherAppWidgetProviderInfo;
|
|
|
|
|
import com.android.launcher3.R;
|
|
|
|
|
import com.android.launcher3.Utilities;
|
2017-01-04 16:31:57 -08:00
|
|
|
import com.android.launcher3.dragndrop.DragLayer;
|
2020-02-19 08:40:49 -08:00
|
|
|
import com.android.launcher3.dragndrop.DraggableView;
|
2019-08-15 14:53:41 -07:00
|
|
|
import com.android.launcher3.util.Executors;
|
2019-06-13 15:01:13 -07:00
|
|
|
import com.android.launcher3.util.Themes;
|
2018-03-14 12:30:11 -07:00
|
|
|
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
|
2013-10-08 19:16:14 -07:00
|
|
|
|
2009-03-03 19:32:27 -08:00
|
|
|
/**
|
|
|
|
|
* {@inheritDoc}
|
|
|
|
|
*/
|
2018-10-12 16:12:29 -07:00
|
|
|
public class LauncherAppWidgetHostView extends NavigableAppWidgetHostView
|
2020-02-19 08:40:49 -08:00
|
|
|
implements TouchCompleteListener, View.OnLongClickListener, DraggableView {
|
2014-07-23 13:58:07 -07:00
|
|
|
|
2016-09-25 21:23:25 -07:00
|
|
|
// Related to the auto-advancing of widgets
|
|
|
|
|
private static final long ADVANCE_INTERVAL = 20000;
|
|
|
|
|
private static final long ADVANCE_STAGGER = 250;
|
|
|
|
|
|
|
|
|
|
// Maintains a list of widget ids which are supposed to be auto advanced.
|
|
|
|
|
private static final SparseBooleanArray sAutoAdvanceWidgetIds = new SparseBooleanArray();
|
|
|
|
|
|
2017-01-04 16:31:57 -08:00
|
|
|
protected final LayoutInflater mInflater;
|
|
|
|
|
|
|
|
|
|
private final CheckLongPressHelper mLongPressHelper;
|
2017-12-18 13:49:44 -08:00
|
|
|
protected final Launcher mLauncher;
|
2014-07-23 13:58:07 -07:00
|
|
|
|
2016-02-09 11:28:52 -08:00
|
|
|
@ViewDebug.ExportedProperty(category = "launcher")
|
2017-12-18 13:49:44 -08:00
|
|
|
private boolean mReinflateOnConfigChange;
|
2011-01-07 15:37:17 -08:00
|
|
|
|
2017-01-04 16:31:57 -08:00
|
|
|
private boolean mIsScrollable;
|
2016-09-25 21:23:25 -07:00
|
|
|
private boolean mIsAttachedToWindow;
|
|
|
|
|
private boolean mIsAutoAdvanceRegistered;
|
|
|
|
|
private Runnable mAutoAdvanceRunnable;
|
|
|
|
|
|
2016-12-19 14:12:05 -08:00
|
|
|
/**
|
|
|
|
|
* The scaleX and scaleY value such that the widget fits within its cellspans, scaleX = scaleY.
|
|
|
|
|
*/
|
|
|
|
|
private float mScaleToFit = 1f;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The translation values to center the widget within its cellspans.
|
|
|
|
|
*/
|
|
|
|
|
private final PointF mTranslationForCentering = new PointF(0, 0);
|
|
|
|
|
|
2009-03-11 12:11:58 -07:00
|
|
|
public LauncherAppWidgetHostView(Context context) {
|
2009-03-03 19:32:27 -08:00
|
|
|
super(context);
|
2017-10-04 16:50:57 -07:00
|
|
|
mLauncher = Launcher.getLauncher(context);
|
2017-01-04 16:31:57 -08:00
|
|
|
mLongPressHelper = new CheckLongPressHelper(this, this);
|
2016-11-04 10:19:58 -07:00
|
|
|
mInflater = LayoutInflater.from(context);
|
2017-10-04 16:50:57 -07:00
|
|
|
setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
|
2016-02-10 12:15:41 -08:00
|
|
|
setBackgroundResource(R.drawable.widget_internal_focus_bg);
|
2016-11-02 11:22:39 -07:00
|
|
|
|
2017-09-11 11:18:03 -07:00
|
|
|
if (Utilities.ATLEAST_OREO) {
|
2019-08-15 14:53:41 -07:00
|
|
|
setExecutor(Executors.THREAD_POOL_EXECUTOR);
|
2016-11-02 11:22:39 -07:00
|
|
|
}
|
2019-06-13 15:01:13 -07:00
|
|
|
if (Utilities.ATLEAST_Q && Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
|
|
|
|
|
setOnLightBackground(true);
|
|
|
|
|
}
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|
2011-05-31 15:52:28 -07:00
|
|
|
|
2017-01-04 16:31:57 -08:00
|
|
|
@Override
|
|
|
|
|
public boolean onLongClick(View view) {
|
|
|
|
|
if (mIsScrollable) {
|
|
|
|
|
DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
|
|
|
|
|
dragLayer.requestDisallowInterceptTouchEvent(false);
|
|
|
|
|
}
|
|
|
|
|
view.performLongClick();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2009-03-03 19:32:27 -08:00
|
|
|
@Override
|
|
|
|
|
protected View getErrorView() {
|
2016-10-07 16:17:19 -07:00
|
|
|
return mInflater.inflate(R.layout.appwidget_error, this, false);
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|
|
|
|
|
|
2012-06-01 17:17:08 -07:00
|
|
|
@Override
|
|
|
|
|
public void updateAppWidget(RemoteViews remoteViews) {
|
|
|
|
|
super.updateAppWidget(remoteViews);
|
2016-09-25 21:23:25 -07:00
|
|
|
|
|
|
|
|
// The provider info or the views might have changed.
|
|
|
|
|
checkIfAutoAdvance();
|
2017-10-04 16:50:57 -07:00
|
|
|
|
|
|
|
|
// It is possible that widgets can receive updates while launcher is not in the foreground.
|
|
|
|
|
// Consequently, the widgets will be inflated for the orientation of the foreground activity
|
|
|
|
|
// (framework issue). On resuming, we ensure that any widgets are inflated for the current
|
|
|
|
|
// orientation.
|
2017-12-18 13:49:44 -08:00
|
|
|
mReinflateOnConfigChange = !isSameOrientation();
|
2017-10-04 16:50:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isSameOrientation() {
|
|
|
|
|
return mLauncher.getResources().getConfiguration().orientation ==
|
|
|
|
|
mLauncher.getOrientation();
|
2017-01-04 16:31:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean checkScrollableRecursively(ViewGroup viewGroup) {
|
|
|
|
|
if (viewGroup instanceof AdapterView) {
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
for (int i=0; i < viewGroup.getChildCount(); i++) {
|
|
|
|
|
View child = viewGroup.getChildAt(i);
|
|
|
|
|
if (child instanceof ViewGroup) {
|
|
|
|
|
if (checkScrollableRecursively((ViewGroup) child)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
2012-06-01 17:17:08 -07:00
|
|
|
}
|
|
|
|
|
|
2009-03-03 19:32:27 -08:00
|
|
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
2013-10-16 10:30:50 -07:00
|
|
|
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
|
2020-03-24 13:55:15 -07:00
|
|
|
DragLayer dragLayer = Launcher.getLauncher(getContext()).getDragLayer();
|
|
|
|
|
if (mIsScrollable) {
|
|
|
|
|
dragLayer.requestDisallowInterceptTouchEvent(true);
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|
2020-03-24 13:55:15 -07:00
|
|
|
dragLayer.setTouchCompleteListener(this);
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|
2020-03-24 13:55:15 -07:00
|
|
|
mLongPressHelper.onTouchEvent(ev);
|
|
|
|
|
return mLongPressHelper.hasPerformedLongPress();
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|
2011-02-18 19:25:06 -08:00
|
|
|
|
2013-08-16 11:10:59 -07:00
|
|
|
public boolean onTouchEvent(MotionEvent ev) {
|
2020-03-24 13:55:15 -07:00
|
|
|
mLongPressHelper.onTouchEvent(ev);
|
2018-12-06 04:01:24 -08:00
|
|
|
// We want to keep receiving though events to be able to cancel long press on ACTION_UP
|
|
|
|
|
return true;
|
2013-08-16 11:10:59 -07:00
|
|
|
}
|
|
|
|
|
|
2014-04-15 15:23:31 -04:00
|
|
|
@Override
|
|
|
|
|
protected void onAttachedToWindow() {
|
|
|
|
|
super.onAttachedToWindow();
|
2016-09-25 21:23:25 -07:00
|
|
|
|
|
|
|
|
mIsAttachedToWindow = true;
|
|
|
|
|
checkIfAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onDetachedFromWindow() {
|
|
|
|
|
super.onDetachedFromWindow();
|
|
|
|
|
|
|
|
|
|
// We can't directly use isAttachedToWindow() here, as this is called before the internal
|
|
|
|
|
// state is updated. So isAttachedToWindow() will return true until next frame.
|
|
|
|
|
mIsAttachedToWindow = false;
|
|
|
|
|
checkIfAutoAdvance();
|
2014-04-15 15:23:31 -04:00
|
|
|
}
|
|
|
|
|
|
2009-04-20 21:03:13 -07:00
|
|
|
@Override
|
|
|
|
|
public void cancelLongPress() {
|
|
|
|
|
super.cancelLongPress();
|
2013-10-08 19:16:14 -07:00
|
|
|
mLongPressHelper.cancelLongPress();
|
|
|
|
|
}
|
2009-04-20 21:03:13 -07:00
|
|
|
|
2014-03-05 18:07:04 -08:00
|
|
|
@Override
|
|
|
|
|
public AppWidgetProviderInfo getAppWidgetInfo() {
|
|
|
|
|
AppWidgetProviderInfo info = super.getAppWidgetInfo();
|
2015-02-17 11:44:15 -08:00
|
|
|
if (info != null && !(info instanceof LauncherAppWidgetProviderInfo)) {
|
2014-03-05 18:07:04 -08:00
|
|
|
throw new IllegalStateException("Launcher widget must have"
|
2015-02-17 11:44:15 -08:00
|
|
|
+ " LauncherAppWidgetProviderInfo");
|
2014-03-05 18:07:04 -08:00
|
|
|
}
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
2013-10-08 19:16:14 -07:00
|
|
|
@Override
|
|
|
|
|
public void onTouchComplete() {
|
2013-10-16 10:30:50 -07:00
|
|
|
if (!mLongPressHelper.hasPerformedLongPress()) {
|
|
|
|
|
// If a long press has been performed, we don't want to clear the record of that since
|
|
|
|
|
// we still may be receiving a touch up which we want to intercept
|
|
|
|
|
mLongPressHelper.cancelLongPress();
|
|
|
|
|
}
|
2009-04-20 21:03:13 -07:00
|
|
|
}
|
2011-01-07 15:37:17 -08:00
|
|
|
|
2016-11-04 10:19:58 -07:00
|
|
|
public void switchToErrorView() {
|
|
|
|
|
// Update the widget with 0 Layout id, to reset the view to error view.
|
|
|
|
|
updateAppWidget(new RemoteViews(getAppWidgetInfo().provider.getPackageName(), 0));
|
|
|
|
|
}
|
|
|
|
|
|
2016-01-25 17:28:37 -08:00
|
|
|
@Override
|
|
|
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
|
|
|
try {
|
|
|
|
|
super.onLayout(changed, left, top, right, bottom);
|
|
|
|
|
} catch (final RuntimeException e) {
|
|
|
|
|
post(new Runnable() {
|
|
|
|
|
@Override
|
|
|
|
|
public void run() {
|
2016-11-04 10:19:58 -07:00
|
|
|
switchToErrorView();
|
2016-01-25 17:28:37 -08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-01-24 11:57:32 -08:00
|
|
|
|
|
|
|
|
mIsScrollable = checkScrollableRecursively(this);
|
2016-01-25 17:28:37 -08:00
|
|
|
}
|
2016-03-10 05:34:30 -08:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
|
|
|
|
super.onInitializeAccessibilityNodeInfo(info);
|
|
|
|
|
info.setClassName(getClass().getName());
|
|
|
|
|
}
|
2016-09-25 21:23:25 -07:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onWindowVisibilityChanged(int visibility) {
|
|
|
|
|
super.onWindowVisibilityChanged(visibility);
|
|
|
|
|
maybeRegisterAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkIfAutoAdvance() {
|
|
|
|
|
boolean isAutoAdvance = false;
|
|
|
|
|
Advanceable target = getAdvanceable();
|
|
|
|
|
if (target != null) {
|
|
|
|
|
isAutoAdvance = true;
|
|
|
|
|
target.fyiWillBeAdvancedByHostKThx();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
boolean wasAutoAdvance = sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0;
|
|
|
|
|
if (isAutoAdvance != wasAutoAdvance) {
|
|
|
|
|
if (isAutoAdvance) {
|
|
|
|
|
sAutoAdvanceWidgetIds.put(getAppWidgetId(), true);
|
|
|
|
|
} else {
|
|
|
|
|
sAutoAdvanceWidgetIds.delete(getAppWidgetId());
|
|
|
|
|
}
|
|
|
|
|
maybeRegisterAutoAdvance();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Advanceable getAdvanceable() {
|
|
|
|
|
AppWidgetProviderInfo info = getAppWidgetInfo();
|
|
|
|
|
if (info == null || info.autoAdvanceViewId == NO_ID || !mIsAttachedToWindow) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
View v = findViewById(info.autoAdvanceViewId);
|
|
|
|
|
return (v instanceof Advanceable) ? (Advanceable) v : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void maybeRegisterAutoAdvance() {
|
|
|
|
|
Handler handler = getHandler();
|
|
|
|
|
boolean shouldRegisterAutoAdvance = getWindowVisibility() == VISIBLE && handler != null
|
|
|
|
|
&& (sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId()) >= 0);
|
|
|
|
|
if (shouldRegisterAutoAdvance != mIsAutoAdvanceRegistered) {
|
|
|
|
|
mIsAutoAdvanceRegistered = shouldRegisterAutoAdvance;
|
|
|
|
|
if (mAutoAdvanceRunnable == null) {
|
2019-08-15 14:53:41 -07:00
|
|
|
mAutoAdvanceRunnable = this::runAutoAdvance;
|
2016-09-25 21:23:25 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handler.removeCallbacks(mAutoAdvanceRunnable);
|
|
|
|
|
scheduleNextAdvance();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void scheduleNextAdvance() {
|
|
|
|
|
if (!mIsAutoAdvanceRegistered) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
long now = SystemClock.uptimeMillis();
|
|
|
|
|
long advanceTime = now + (ADVANCE_INTERVAL - (now % ADVANCE_INTERVAL)) +
|
|
|
|
|
ADVANCE_STAGGER * sAutoAdvanceWidgetIds.indexOfKey(getAppWidgetId());
|
|
|
|
|
Handler handler = getHandler();
|
|
|
|
|
if (handler != null) {
|
|
|
|
|
handler.postAtTime(mAutoAdvanceRunnable, advanceTime);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void runAutoAdvance() {
|
|
|
|
|
Advanceable target = getAdvanceable();
|
|
|
|
|
if (target != null) {
|
|
|
|
|
target.advance();
|
|
|
|
|
}
|
|
|
|
|
scheduleNextAdvance();
|
|
|
|
|
}
|
2016-12-19 14:12:05 -08:00
|
|
|
|
|
|
|
|
public void setScaleToFit(float scale) {
|
|
|
|
|
mScaleToFit = scale;
|
|
|
|
|
setScaleX(scale);
|
|
|
|
|
setScaleY(scale);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float getScaleToFit() {
|
|
|
|
|
return mScaleToFit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setTranslationForCentering(float x, float y) {
|
|
|
|
|
mTranslationForCentering.set(x, y);
|
|
|
|
|
setTranslationX(x);
|
|
|
|
|
setTranslationY(y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PointF getTranslationForCentering() {
|
|
|
|
|
return mTranslationForCentering;
|
|
|
|
|
}
|
2017-10-04 16:50:57 -07:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void onConfigurationChanged(Configuration newConfig) {
|
|
|
|
|
super.onConfigurationChanged(newConfig);
|
|
|
|
|
|
2017-12-18 13:49:44 -08:00
|
|
|
// Only reinflate when the final configuration is same as the required configuration
|
|
|
|
|
if (mReinflateOnConfigChange && isSameOrientation()) {
|
|
|
|
|
mReinflateOnConfigChange = false;
|
2018-02-27 19:20:15 -08:00
|
|
|
reInflate();
|
2017-10-04 16:50:57 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-18 13:49:44 -08:00
|
|
|
public void reInflate() {
|
2018-02-27 19:20:15 -08:00
|
|
|
if (!isAttachedToWindow()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-10-04 16:50:57 -07:00
|
|
|
LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) getTag();
|
|
|
|
|
// Remove and rebind the current widget (which was inflated in the wrong
|
|
|
|
|
// orientation), but don't delete it from the database
|
|
|
|
|
mLauncher.removeItem(this, info, false /* deleteFromDb */);
|
|
|
|
|
mLauncher.bindAppWidget(info);
|
|
|
|
|
}
|
2018-10-12 16:12:29 -07:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected boolean shouldAllowDirectClick() {
|
|
|
|
|
if (getTag() instanceof ItemInfo) {
|
|
|
|
|
ItemInfo item = (ItemInfo) getTag();
|
|
|
|
|
return item.spanX == 1 && item.spanY == 1;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2020-02-19 08:40:49 -08:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public int getViewType() {
|
|
|
|
|
return DRAGGABLE_WIDGET;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void getVisualDragBounds(Rect bounds) {
|
2020-04-08 18:00:30 -07:00
|
|
|
int width = (int) (getMeasuredWidth() * mScaleToFit);
|
|
|
|
|
int height = (int) (getMeasuredHeight() * mScaleToFit);
|
|
|
|
|
|
|
|
|
|
bounds.set(0, 0 , width, height);
|
2020-02-19 08:40:49 -08:00
|
|
|
}
|
2009-03-03 19:32:27 -08:00
|
|
|
}
|