/* * Copyright (C) 2011 The original author or authors. * * 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.zapta.apps.maniana.widget; import java.io.File; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.net.Uri; import android.os.SystemClock; import android.text.format.DateUtils; import android.text.format.Time; import android.view.LayoutInflater; import android.view.View; import android.view.View.MeasureSpec; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.zapta.apps.maniana.R; import com.zapta.apps.maniana.annotations.ApplicationScope; import com.zapta.apps.maniana.model.AppModel; import com.zapta.apps.maniana.model.ItemModelReadOnly; import com.zapta.apps.maniana.services.MainActivityServices; import com.zapta.apps.maniana.settings.DateOrder; import com.zapta.apps.maniana.settings.ItemFontVariation; import com.zapta.apps.maniana.util.BitmapUtil; import com.zapta.apps.maniana.util.DisplayUtil; import com.zapta.apps.maniana.util.FileUtil; import com.zapta.apps.maniana.util.Orientation; import com.zapta.apps.maniana.util.TextUtil; import com.zapta.apps.maniana.view.ExtendedTextView; import com.zapta.apps.maniana.widget.ListWidgetSize.OrientationInfo; /** * * List widget template to be drawn as a bitmap. It does not include the paper background which is * added later at the remoteviews level. * * @author Tal Dayan */ @ApplicationScope public class ListWidgetProviderTemplate { /** Will scale item text size down to this size in SP units. */ private static final int MIN_NORMALIZED_TEXT_SIZE = 10; @Nullable private final AppModel mModel; private final Time mSometimeToday; private final Context mContext; private final DateOrder mDateOrder; private final float mDensity; private final LinearLayout mTopView; private final View mBackgroundColorView; private final View mToolbarView; private final TextView mToolbarTitleTextView; private final LinearLayout mItemListView; private final FrameLayout mListTopSpaceView; private final List<TextView> mItemTextViews; private final LayoutInflater mLayoutInflater; // Preferences private final ItemFontVariation mFontVariationPreference; private final boolean mAutoFitPreference; private final boolean mPaperPreference; private final int mBackgroundColorPreference; private final boolean mToolbarEanbledPreference; private final boolean mToolbarShowDatePreference; private final boolean mIncludeCompletedItemsPreference; private final boolean mSingleLinePreference; public ListWidgetProviderTemplate(Context context, @Nullable AppModel model, Time sometimeToday, boolean paperPreference, int backgroundColorPreference, boolean toolbarEanbledPreference, boolean toolbarShowDatePreference, boolean includeCompletedItemsPreference, boolean singleLinePreference, ItemFontVariation fontVariationPreference, boolean autoFitPreference) { mContext = context; mDateOrder = DateOrder.localDateOrder(context); mDensity = DisplayUtil.getDensity(context); mModel = model; mSometimeToday = sometimeToday; mPaperPreference = paperPreference; mBackgroundColorPreference = backgroundColorPreference; mToolbarEanbledPreference = toolbarEanbledPreference; mToolbarShowDatePreference = toolbarShowDatePreference; mIncludeCompletedItemsPreference = includeCompletedItemsPreference; mSingleLinePreference = singleLinePreference; mFontVariationPreference = fontVariationPreference; mAutoFitPreference = autoFitPreference; // TODO: need this only is using auto shrink mLayoutInflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); mTopView = (LinearLayout) mLayoutInflater.inflate(R.layout.widget_list_template_layout, null); mBackgroundColorView = mTopView.findViewById(R.id.widget_list_background_color); mToolbarView = mTopView.findViewById(R.id.widget_list_template_toolbar); mToolbarTitleTextView = (TextView) mToolbarView .findViewById(R.id.widget_list_template_toolbar_title); mListTopSpaceView = (FrameLayout) mTopView .findViewById(R.id.widget_list_template_list_top_space); mItemListView = (LinearLayout) mTopView.findViewById(R.id.widget_list_template_item_list); mItemTextViews = new ArrayList<TextView>(); // Set template background. This can be the background solid color or the paper color. mBackgroundColorView.setBackgroundColor(mBackgroundColorPreference); // Set template view item list populateTemplateItemList(); } /** Set the image of a single orientation. */ public final Uri renderOrientation(ListWidgetSize listWidgetSize, Orientation orientation, int widgetWidthPixels, int widgetHeightPixels, @Nullable PaperBackground paperBackground) { final OrientationInfo orientationInfo = orientation.isPortrait ? listWidgetSize.portraitInfo : listWidgetSize.landscapeInfo; // Does not set title size. This is done later. // TODO: refactor out to a method final String titleText = mToolbarEanbledPreference ? (mToolbarShowDatePreference ? (mSometimeToday .format(orientationInfo.dateFormat.formatString(mDateOrder))) : mContext .getString(R.string.page_title_Today)).toUpperCase() : null; setToolbar(titleText); final int shadowRightPixels = mPaperPreference ? paperBackground .shadowRightPixels(widgetWidthPixels) : 0; final int shadowBottomPixels = mPaperPreference ? paperBackground .shadowBottomPixels(widgetHeightPixels) : 0; // Set padding to match the drop shadow portion of paper background, if used. mTopView.setPadding(0, 0, shadowRightPixels, shadowBottomPixels); resizeToFit(widgetWidthPixels, widgetHeightPixels, shadowBottomPixels, orientationInfo); // NTOE: ARGB_4444 results in a smaller file than ARGB_8888 (e.g. 50K vs 150k) // but does not look as good. final Bitmap bitmap1 = Bitmap.createBitmap(widgetWidthPixels, widgetHeightPixels, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap1); mTopView.draw(canvas); final int ROUND_CORNER_RADIUS_DIPS = 4; // NOTE: rounding the bitmap here when paper background is selected will do nothing // since the paper background is added later via the remote views. final Bitmap bitmap2; if (mPaperPreference) { bitmap2 = bitmap1; } else { bitmap2 = BitmapUtil.roundCornersRGB888(bitmap1, (int) (ROUND_CORNER_RADIUS_DIPS * mDensity + 0.5f)); } // NOTE: RemoteViews class has an issue with transferring large bitmaps. As a workaround, we // transfer the bitmap using a file URI. We could transfer small widgets directly // as bitmap but use file based transfer for all sizes for the sake of simplicity. // For more information on this issue see http://tinyurl.com/75jh2yf final String fileName = orientationInfo.imageFileName; // We make the file world readable so the home launcher can pull it via the file URI. // TODO: if there are security concerns about having this file readable, append to it // a long random suffix and cleanup the old ones. FileUtil.writeBitmapToPngFile(mContext, bitmap2, fileName, true); final Uri fileUri = Uri.fromFile(new File(mContext.getFilesDir(), fileName)); return fileUri; } // Resize the template in preparation for rendering. private final void resizeToFit(int widgetWidthPixels, int widgetHeightPixels, int bottomPadding, OrientationInfo orientationInfo) { // DebugTimer timer = new DebugTimer(); final int minItemTextSize = mAutoFitPreference ? MIN_NORMALIZED_TEXT_SIZE : mFontVariationPreference.getTextSize(); setSingleLine(mSingleLinePreference); boolean fit = autoResizeText(widgetWidthPixels, widgetHeightPixels, minItemTextSize, mFontVariationPreference.getTextSize(), orientationInfo.maxTitleTextSizeSp); if (fit || !mAutoFitPreference) { // timer.report("Fit done"); return; } if (!mSingleLinePreference) { setSingleLine(true); fit = autoResizeText(widgetWidthPixels, widgetHeightPixels, minItemTextSize, mFontVariationPreference.getTextSize(), orientationInfo.maxTitleTextSizeSp); } // timer.report("Fit done"); } /** * Try to to scale down the text size from given max to min until it fit. Returns true if fit. * * TODO: consider to use binary search over text size range. */ private final boolean autoResizeText(int widgetWidthPixels, int widgetHeightPixels, int minItemTextSize, int maxItemTextSize, int maxTitleTextSize) { // Try min, size, return if no fit. if (!resizeText(widgetWidthPixels, widgetHeightPixels, minItemTextSize, maxTitleTextSize)) { return false; } // Try the max size. Return if fits. if (resizeText(widgetWidthPixels, widgetHeightPixels, maxItemTextSize, maxTitleTextSize)) { return true; } // Use binary search. high >= current >= min float highSize = maxItemTextSize; float lowSize = minItemTextSize; float currentSize = minItemTextSize; for (;;) { // Termination condition if ((highSize - lowSize) < 0.5f) { // The size we want to have upon return is lowSize. If this is not // the current size than do one more resizing. We use a safe flaot // comparison. if (Math.abs(lowSize - currentSize) > 0.1f) { resizeText(widgetWidthPixels, widgetHeightPixels, lowSize, maxTitleTextSize); } return true; } // Test mid size currentSize = (lowSize + highSize) / 2; final boolean currentSizeFits = resizeText(widgetWidthPixels, widgetHeightPixels, currentSize, maxTitleTextSize); if (currentSizeFits) { lowSize = currentSize; } else { highSize = currentSize; } } } /** * Resize template. Returns true if fit. Upon return, template view is ready to be rendered onto * a canvas. */ private final boolean resizeText(int widgetWidthPixels, int widgetHeightPixels, float itemTextSizeSp, float maxTitleTextSize) { if (mToolbarEanbledPreference) { final float proposedTitleTextSizeSp = itemTextSizeSp * 0.8f; final float titleTextSizeSp = Math.max(ListWidgetSize.MAX_TITLE_TEXT_SIZE_SP, Math.min(proposedTitleTextSizeSp, maxTitleTextSize)); mToolbarTitleTextView.setTextSize(titleTextSizeSp); } for (TextView itemTextView : mItemTextViews) { itemTextView.setTextSize(itemTextSizeSp); } // Set the space above first item. It looks better this way. The space is proportional // to text size and screen density. { final float K = 0.45f; final int topSpacePixels = (int) (itemTextSizeSp * mDensity * K + 0.5f); mListTopSpaceView.getLayoutParams().height = topSpacePixels; } mTopView.measure(MeasureSpec.makeMeasureSpec(widgetWidthPixels, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(widgetHeightPixels, MeasureSpec.EXACTLY)); // TODO: subtract '1' from ends? mTopView.layout(0, 0, widgetWidthPixels, widgetHeightPixels); // We use margin height proportional to the text size. This way it is intuitive // to the user that this is the last line and there are no more lines beyond the // wieget bottom. final int minMarginPixels = (int) (itemTextSizeSp * mDensity); return mItemListView.getBottom() < (mBackgroundColorView.getHeight() - minMarginPixels); } private final void setSingleLine(boolean singleLine) { for (TextView itemTextView : mItemTextViews) { // NOTE: TextView has a bug that does not allows more than two lines when using // ellipsize. Otherwise we would give the user more choices about the max number of // lines. More details here: http://code.google.com/p/android/issues/detail?id=2254 if (singleLine) { itemTextView.setSingleLine(true); itemTextView.setMaxLines(1); } else { itemTextView.setSingleLine(false); // NOTE: on ICS (API 14) the text view behaves differently and does not limit the // lines to two when ellipsize. For consistency, we limit it explicitly to two // lines. itemTextView.setMaxLines(2); } } } /** * Populate the given template layout with task data and/or informative messages. */ private final void populateTemplateItemList() { // For debugging final boolean debugTimestamp = false; if (debugTimestamp) { final String message = String.format("[%s]", SystemClock.elapsedRealtime() / 1000); addTemplateMessageItem(message); } if (mModel == null) { addTemplateMessageItem("(" + mContext.getString(R.string.widget_Maniana_data_not_found) + ")"); return; } final List<ItemModelReadOnly> items = WidgetUtil.selectTodaysItems(mModel, mIncludeCompletedItemsPreference); // If no items, add a message and leave. if (items.isEmpty()) { final String emptyMessage = "(" + mContext .getString(mIncludeCompletedItemsPreference ? R.string.widget_no_tasks : R.string.widget_no_active_tasks) + ")"; addTemplateMessageItem(emptyMessage); return; } // Add items. for (ItemModelReadOnly item : items) { final LinearLayout itemView = (LinearLayout) mLayoutInflater.inflate( R.layout.widget_list_template_item_layout, null); final ExtendedTextView extendedTextView = (ExtendedTextView) itemView .findViewById(R.id.widget_item_text_view); final View itemColorView = itemView.findViewById(R.id.widget_item_color); String text = item.getText(); /* FIXME: do we need this? what's the above function for? if (item.getScheduledTime() != 0) { text = DateUtils.formatDateTime(mContext, item.getScheduledTime(), DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_SHOW_TIME) + " " + text; } */ extendedTextView.setText(text); TextUtil.ICS_HACK_TEXT_VIEW(extendedTextView); mFontVariationPreference.apply(extendedTextView, item.isCompleted(), true); // For debugging. Highlight each // { // final int bgColor = 0x33000000 | RandomUtil.random.nextInt(0x1000000); // extendedTextView.setBackgroundColor(bgColor); // } // If color is NONE show a gray solid color to help visually // grouping item text lines. itemColorView.setBackgroundColor(item.getColor().getColor(0xff808080)); mItemListView.addView(itemView); mItemTextViews.add(extendedTextView); } } /** * Add an informative message to the item list. These messages are formatted differently than * actual tasks. */ private final void addTemplateMessageItem(String message) { final LinearLayout itemView = (LinearLayout) mLayoutInflater.inflate( R.layout.widget_list_template_item_layout, null); final ExtendedTextView extendedTextView = (ExtendedTextView) itemView .findViewById(R.id.widget_item_text_view); final View colorView = itemView.findViewById(R.id.widget_item_color); // TODO: setup message text using widget font size preference? extendedTextView.setSingleLine(false); extendedTextView.setText(message); TextUtil.ICS_HACK_TEXT_VIEW(extendedTextView); mFontVariationPreference.apply(extendedTextView, false, true); colorView.setVisibility(View.GONE); mItemListView.addView(itemView); mItemTextViews.add(extendedTextView); } /** * Set the toolbar portion of the template view. * * showToolbarBackground and titleSize are ignored if not toolbarEnabled. titleSize. */ private final void setToolbar(String titleText) { if (!mToolbarEanbledPreference) { mToolbarView.setVisibility(View.GONE); return; } mToolbarView.setVisibility(View.VISIBLE); // Show or hide toolbar background. if (mPaperPreference) { mToolbarView.setBackgroundColor(0x00000000); } else { mToolbarView.setBackgroundResource(R.drawable.widget_toolbar_background); } // Show title mToolbarTitleTextView.setText(titleText); TextUtil.ICS_HACK_TEXT_VIEW(mToolbarTitleTextView); // The voice recognition button is shown only if this device supports voice recognition. final View templateAddTextByVoiceButton = mToolbarView .findViewById(R.id.widget_list_template_toolbar_add_by_voice); if (MainActivityServices.isVoiceRecognitionSupported(mContext)) { templateAddTextByVoiceButton.setVisibility(View.VISIBLE); } else { templateAddTextByVoiceButton.setVisibility(View.GONE); } } }