/*
* Copyright 2014 Google Inc. All rights reserved.
*
* 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.google.samples.apps.iosched.util;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.*;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import android.text.Html;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.StyleSpan;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.google.samples.apps.iosched.BuildConfig;
import com.google.samples.apps.iosched.Config;
import com.google.samples.apps.iosched.R;
import com.google.samples.apps.iosched.provider.ScheduleContract;
import com.google.samples.apps.iosched.provider.ScheduleContract.Rooms;
import com.google.samples.apps.iosched.ui.phone.MapActivity;
import com.google.samples.apps.iosched.ui.tablet.MapMultiPaneActivity;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Formatter;
import java.util.TimeZone;
import java.util.regex.Pattern;
import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;
/**
* An assortment of UI helpers.
*/
public class UIUtils {
private static final String TAG = makeLogTag(UIUtils.class);
/**
* Factor applied to session color to derive the background color on panels and when
* a session photo could not be downloaded (or while it is being downloaded)
*/
public static final float SESSION_BG_COLOR_SCALE_FACTOR = 0.65f;
public static final float SESSION_PHOTO_SCRIM_ALPHA = 0.75f;
public static final String TARGET_FORM_FACTOR_ACTIVITY_METADATA =
"com.google.samples.apps.iosched.meta.TARGET_FORM_FACTOR";
public static final String TARGET_FORM_FACTOR_HANDSET = "handset";
public static final String TARGET_FORM_FACTOR_TABLET = "tablet";
/**
* Flags used with {@link DateUtils#formatDateRange}.
*/
private static final int TIME_FLAGS = DateUtils.FORMAT_SHOW_TIME
| DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY;
/**
* Regex to search for HTML escape sequences.
*
* <p></p>Searches for any continuous string of characters starting with an ampersand and ending with a
* semicolon. (Example: &)
*/
private static final Pattern REGEX_HTML_ESCAPE = Pattern.compile(".*&\\S;.*");
private static CharSequence sNowPlayingText;
private static CharSequence sLivestreamNowText;
private static CharSequence sLivestreamAvailableText;
public static final String GOOGLE_PLUS_PACKAGE_NAME = "com.google.android.apps.plus";
public static final String YOUTUBE_PACKAGE_NAME = "com.google.android.youtube";
public static final int ANIMATION_FADE_IN_TIME = 250;
public static final String TRACK_ICONS_TAG = "tracks";
private static SimpleDateFormat sDayOfWeekFormat = new SimpleDateFormat("E");
private static DateFormat sShortTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
public static String formatSessionSubtitle(long intervalStart, long intervalEnd, String roomName, StringBuilder recycle,
Context context) {
return formatSessionSubtitle(intervalStart, intervalEnd, roomName, recycle, context, false);
}
/**
* Format and return the given session time and {@link Rooms} values using
* {@link Config#CONFERENCE_TIMEZONE}.
*/
public static String formatSessionSubtitle(long intervalStart, long intervalEnd, String roomName, StringBuilder recycle,
Context context, boolean shortFormat) {
// Determine if the session is in the past
long currentTimeMillis = UIUtils.getCurrentTime(context);
boolean conferenceEnded = currentTimeMillis > Config.CONFERENCE_END_MILLIS;
boolean sessionEnded = currentTimeMillis > intervalEnd;
if (sessionEnded && !conferenceEnded) {
return context.getString(R.string.session_finished);
}
if (roomName == null) {
roomName = context.getString(R.string.unknown_room);
}
if (shortFormat) {
Date intervalStartDate = new Date(intervalStart);
sDayOfWeekFormat.setTimeZone(PrefUtils.getDisplayTimeZone(context));
sShortTimeFormat.setTimeZone(PrefUtils.getDisplayTimeZone(context));
return sDayOfWeekFormat.format(intervalStartDate) + " "
+ sShortTimeFormat.format(intervalStartDate);
} else {
return context.getString(R.string.session_subtitle,
formatIntervalTimeString(intervalStart, intervalEnd, recycle, context), roomName);
}
}
/**
* Format and return the given session speakers and {@link Rooms} values.
*/
public static String formatSessionSubtitle(String roomName, String speakerNames,
Context context) {
// Determine if the session is in the past
if (roomName == null) {
roomName = context.getString(R.string.unknown_room);
}
if (!TextUtils.isEmpty(speakerNames)) {
return speakerNames + "\n" + roomName;
} else {
return roomName;
}
}
/**
* Format and return the given time interval using {@link Config#CONFERENCE_TIMEZONE}
* (unless local time was explicitly requested by the user).
*/
public static String formatIntervalTimeString(long intervalStart, long intervalEnd,
StringBuilder recycle, Context context) {
if (recycle == null) {
recycle = new StringBuilder();
} else {
recycle.setLength(0);
}
Formatter formatter = new Formatter(recycle);
return DateUtils.formatDateRange(context, formatter, intervalStart, intervalEnd, TIME_FLAGS,
PrefUtils.getDisplayTimeZone(context).getID()).toString();
}
public static boolean isSameDayDisplay(long time1, long time2, Context context) {
TimeZone displayTimeZone = PrefUtils.getDisplayTimeZone(context);
Calendar cal1 = Calendar.getInstance(displayTimeZone);
Calendar cal2 = Calendar.getInstance(displayTimeZone);
cal1.setTimeInMillis(time1);
cal2.setTimeInMillis(time2);
return cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
&& cal1.get(Calendar.DAY_OF_YEAR) == cal2.get(Calendar.DAY_OF_YEAR);
}
/**
* Populate the given {@link TextView} with the requested text, formatting
* through {@link Html#fromHtml(String)} when applicable. Also sets
* {@link TextView#setMovementMethod} so inline links are handled.
*/
public static void setTextMaybeHtml(TextView view, String text) {
if (TextUtils.isEmpty(text)) {
view.setText("");
return;
}
if ((text.contains("<") && text.contains(">")) || REGEX_HTML_ESCAPE.matcher(text).find()) {
view.setText(Html.fromHtml(text));
view.setMovementMethod(LinkMovementMethod.getInstance());
} else {
view.setText(text);
}
}
public static String getLiveBadgeText(final Context context, long start, long end) {
long now = getCurrentTime(context);
if (now < start) {
// Will be live later
return context.getString(R.string.live_available);
} else if (start <= now && now <= end) {
// Live right now!
// Indicated by a visual live now badge
return "";
} else {
// Too late.
return "";
}
}
/**
* Given a snippet string with matching segments surrounded by curly
* braces, turn those areas into bold spans, removing the curly braces.
*/
public static Spannable buildStyledSnippet(String snippet) {
final SpannableStringBuilder builder = new SpannableStringBuilder(snippet);
// Walk through string, inserting bold snippet spans
int startIndex, endIndex = -1, delta = 0;
while ((startIndex = snippet.indexOf('{', endIndex)) != -1) {
endIndex = snippet.indexOf('}', startIndex);
// Remove braces from both sides
builder.delete(startIndex - delta, startIndex - delta + 1);
builder.delete(endIndex - delta - 1, endIndex - delta);
// Insert bold style
builder.setSpan(new StyleSpan(Typeface.BOLD),
startIndex - delta, endIndex - delta - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
//builder.setSpan(new ForegroundColorSpan(0xff111111),
// startIndex - delta, endIndex - delta - 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
delta += 2;
}
return builder;
}
public static void preferPackageForIntent(Context context, Intent intent, String packageName) {
PackageManager pm = context.getPackageManager();
for (ResolveInfo resolveInfo : pm.queryIntentActivities(intent, 0)) {
if (resolveInfo.activityInfo.packageName.equals(packageName)) {
intent.setPackage(packageName);
break;
}
}
}
public static String getSessionHashtagsString(String hashtags) {
if (!TextUtils.isEmpty(hashtags)) {
if (!hashtags.startsWith("#")) {
hashtags = "#" + hashtags;
}
if (hashtags.contains(Config.CONFERENCE_HASHTAG_PREFIX)) {
return hashtags;
}
return Config.CONFERENCE_HASHTAG + " " + hashtags;
} else {
return Config.CONFERENCE_HASHTAG;
}
}
private static final int BRIGHTNESS_THRESHOLD = 130;
/**
* Calculate whether a color is light or dark, based on a commonly known
* brightness formula.
*
* @see {@literal http://en.wikipedia.org/wiki/HSV_color_space%23Lightness}
*/
public static boolean isColorDark(int color) {
return ((30 * Color.red(color) +
59 * Color.green(color) +
11 * Color.blue(color)) / 100) <= BRIGHTNESS_THRESHOLD;
}
public static boolean isTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout
& Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_LARGE;
}
// Whether a feedback notification was fired for a particular session. In the event that a
// feedback notification has not been fired yet, return false and set the bit.
public static boolean isFeedbackNotificationFiredForSession(Context context, String sessionId) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String key = String.format("feedback_notification_fired_%s", sessionId);
boolean fired = sp.getBoolean(key, false);
sp.edit().putBoolean(key, true).commit();
return fired;
}
// Clear the flag that says a notification was fired for the given session.
// Typically used to debug notifications.
public static void unmarkFeedbackNotificationFiredForSession(Context context, String sessionId) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String key = String.format("feedback_notification_fired_%s", sessionId);
sp.edit().putBoolean(key, false).commit();
}
// Shows whether a notification was fired for a particular session time block. In the
// event that notification has not been fired yet, return false and set the bit.
public static boolean isNotificationFiredForBlock(Context context, String blockId) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
final String key = String.format("notification_fired_%s", blockId);
boolean fired = sp.getBoolean(key, false);
sp.edit().putBoolean(key, true).commit();
return fired;
}
private static final long sAppLoadTime = System.currentTimeMillis();
public static long getCurrentTime(final Context context) {
if (BuildConfig.DEBUG) {
return context.getSharedPreferences("mock_data", Context.MODE_PRIVATE)
.getLong("mock_current_time", System.currentTimeMillis())
+ System.currentTimeMillis() - sAppLoadTime;
// return ParserUtils.parseTime("2012-06-27T09:44:45.000-07:00")
// + System.currentTimeMillis() - sAppLoadTime;
} else {
return System.currentTimeMillis();
}
}
public static boolean shouldShowLiveSessionsOnly(final Context context) {
return !PrefUtils.isAttendeeAtVenue(context)
&& getCurrentTime(context) < Config.CONFERENCE_END_MILLIS;
}
/**
* Enables and disables {@linkplain android.app.Activity activities} based on their
* {@link #TARGET_FORM_FACTOR_ACTIVITY_METADATA}" meta-data and the current device.
* Values should be either "handset", "tablet", or not present (meaning universal).
* <p>
* <a href="http://stackoverflow.com/questions/13202805">Original code</a> by Dandre Allison.
* @param context the current context of the device
* @see #isHoneycombTablet(android.content.Context)
*/
public static void enableDisableActivitiesByFormFactor(Context context) {
final PackageManager pm = context.getPackageManager();
boolean isTablet = isTablet(context);
try {
PackageInfo pi = pm.getPackageInfo(context.getPackageName(),
PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA);
if (pi == null) {
LOGE(TAG, "No package info found for our own package.");
return;
}
final ActivityInfo[] activityInfos = pi.activities;
for (ActivityInfo info : activityInfos) {
String targetDevice = null;
if (info.metaData != null) {
targetDevice = info.metaData.getString(TARGET_FORM_FACTOR_ACTIVITY_METADATA);
}
boolean tabletActivity = TARGET_FORM_FACTOR_TABLET.equals(targetDevice);
boolean handsetActivity = TARGET_FORM_FACTOR_HANDSET.equals(targetDevice);
boolean enable = !(handsetActivity && isTablet)
&& !(tabletActivity && !isTablet);
String className = info.name;
pm.setComponentEnabledSetting(
new ComponentName(context, Class.forName(className)),
enable
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
} catch (PackageManager.NameNotFoundException e) {
LOGE(TAG, "No package info found for our own package.", e);
} catch (ClassNotFoundException e) {
LOGE(TAG, "Activity not found within package.", e);
}
}
public static Class getMapActivityClass(Context context) {
if (UIUtils.isTablet(context)) {
return MapMultiPaneActivity.class;
}
return MapActivity.class;
}
/**
* If an activity's intent is for a Google I/O web URL that the app can handle
* natively, this method translates the intent to the equivalent native intent.
*/
public static void tryTranslateHttpIntent(Activity activity) {
Intent intent = activity.getIntent();
if (intent == null) {
return;
}
Uri uri = intent.getData();
if (uri == null || TextUtils.isEmpty(uri.getPath())) {
return;
}
String prefixPath = Config.SESSION_DETAIL_WEB_URL_PREFIX.getPath();
String path = uri.getPath();
if (Config.SESSION_DETAIL_WEB_URL_PREFIX.getScheme().equals(uri.getScheme()) &&
Config.SESSION_DETAIL_WEB_URL_PREFIX.getHost().equals(uri.getHost()) &&
path.startsWith(prefixPath)) {
String sessionId = path.substring(prefixPath.length());
activity.setIntent(new Intent(
Intent.ACTION_VIEW,
ScheduleContract.Sessions.buildSessionUri(sessionId)));
}
}
private static final int[] RES_IDS_ACTION_BAR_SIZE = { android.R.attr.actionBarSize };
/** Calculates the Action Bar height in pixels. */
public static int calculateActionBarSize(Context context) {
if (context == null) {
return 0;
}
Resources.Theme curTheme = context.getTheme();
if (curTheme == null) {
return 0;
}
TypedArray att = curTheme.obtainStyledAttributes(RES_IDS_ACTION_BAR_SIZE);
if (att == null) {
return 0;
}
float size = att.getDimension(0, 0);
att.recycle();
return (int) size;
}
public static int setColorAlpha(int color, float alpha) {
int alpha_int = Math.min(Math.max((int)(alpha * 255.0f), 0), 255);
return Color.argb(alpha_int, Color.red(color), Color.green(color), Color.blue(color));
}
public static int scaleColor(int color, float factor, boolean scaleAlpha) {
return Color.argb(scaleAlpha ? (Math.round(Color.alpha(color) * factor)) : Color.alpha(color),
Math.round(Color.red(color) * factor), Math.round(Color.green(color) * factor),
Math.round(Color.blue(color) * factor));
}
public static int scaleSessionColorToDefaultBG(int color) {
return scaleColor(color, SESSION_BG_COLOR_SCALE_FACTOR, false);
}
public static boolean hasActionBar(Activity activity) {
return activity.getActionBar() != null;
}
public static void showHashtagStream(final Context context, String hashTag) {
final String hashTagsString = getSessionHashtagsString(hashTag);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(
"https://plus.google.com/s/"
+ hashTagsString.replaceAll(" ", "%20").replaceAll("#", "%23")));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
preferPackageForIntent(context, intent, UIUtils.GOOGLE_PLUS_PACKAGE_NAME);
context.startActivity(intent);
}
public static void setStartPadding(final Context context, View view, int padding) {
if (isRtl(context)) {
view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), padding, view.getPaddingBottom());
} else {
view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom());
}
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public static boolean isRtl(final Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
return false;
} else {
return context.getResources().getConfiguration().getLayoutDirection()
== View.LAYOUT_DIRECTION_RTL;
}
}
public static void setAccessibilityIgnore(View view) {
view.setClickable(false);
view.setFocusable(false);
view.setContentDescription("");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
public static void setUpButterBar(View butterBar, String messageText, String actionText,
View.OnClickListener listener) {
if (butterBar == null) {
LOGE(TAG, "Failed to set up butter bar: it's null.");
return;
}
TextView textView = (TextView) butterBar.findViewById(R.id.butter_bar_text);
if (textView != null) {
textView.setText(messageText);
}
Button button = (Button) butterBar.findViewById(R.id.butter_bar_button);
if (button != null) {
button.setText(actionText == null ? "" : actionText);
button.setVisibility(!TextUtils.isEmpty(actionText) ? View.VISIBLE : View.GONE);
}
button.setOnClickListener(listener);
butterBar.setVisibility(View.VISIBLE);
}
public static float getProgress(int value, int min, int max) {
if (min == max) {
throw new IllegalArgumentException("Max (" + max + ") cannot equal min (" + min + ")");
}
return (value - min) / (float) (max - min);
}
}