/* * Copyright 2015 Synced Synapse. 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 org.xbmc.kore.utils; import android.animation.Animator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Rect; import android.os.Handler; import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewAnimationUtils; import android.view.WindowManager; import android.widget.GridLayout; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import org.xbmc.kore.R; import org.xbmc.kore.Settings; import org.xbmc.kore.host.HostInfo; import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.type.GlobalType; import org.xbmc.kore.jsonrpc.type.VideoType; import org.xbmc.kore.ui.sections.remote.RemoteActivity; import java.io.File; import java.util.ArrayList; import java.util.List; /** * General UI Utils */ public class UIUtils { public static final float IMAGE_RESIZE_FACTOR = 1.0f; public static final int initialButtonRepeatInterval = 400; // ms public static final int buttonRepeatInterval = 80; // ms public static final int buttonVibrationDuration = 50; //ms /** * Formats time based on seconds * @param seconds seconds * @return Formated string */ public static String formatTime(int seconds) { return formatTime(seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60); } /** * Formats time */ public static String formatTime(GlobalType.Time time) { return formatTime(time.hours, time.minutes, time.seconds); } /** * Formats time */ public static String formatTime(int hours, int minutes, int seconds) { if (hours > 0) { return String.format("%d:%02d:%02d", hours, minutes, seconds); } else { return String.format("%1d:%02d",minutes, seconds); } } /** * Formats a file size, ISO prefixes */ public static String formatFileSize(int bytes) { if (bytes <= 0) return null; if (bytes < 1024) { return bytes + "B"; } else if (bytes < 1024 * 1024) { return String.format("%.1f KB", bytes / 1024.0); } else if (bytes < 1024 * 1024 * 1024) { return String.format("%.1f MB", bytes / (1024.0 * 1024.0)); } else { return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0)); } } /** * Loads an image into an imageview * @param hostManager Hostmanager connected to the host * @param imageUrl XBMC url of the image to load * @param imageView Image view to load into * @param imageWidth Width of the image, for caching purposes * @param imageHeight Height of the image, for caching purposes */ public static void loadImageIntoImageview(HostManager hostManager, String imageUrl, ImageView imageView, int imageWidth, int imageHeight) { // if (TextUtils.isEmpty(imageUrl)) { // imageView.setImageResource(R.drawable.delete_ic_action_picture); // return; // } if ((imageWidth) > 0 && (imageHeight > 0)) { hostManager.getPicasso() .load(hostManager.getHostInfo().getImageUrl(imageUrl)) .resize(imageWidth, imageHeight) .centerCrop() .into(imageView); } else { hostManager.getPicasso() .load(hostManager.getHostInfo().getImageUrl(imageUrl)) .fit() .centerCrop() .into(imageView); } } private static TypedArray characterAvatarColors = null; // private static Random randomGenerator = new Random(); /** * Loads an image into an imageview, presenting an alternate charater avatar if empty * @param hostManager Hostmanager connected to the host * @param imageUrl XBMC url of the image to load * @param stringAvatar Character avatar too present if image is null * @param imageView Image view to load into * @param imageWidth Width of the image, for caching purposes * @param imageHeight Height of the image, for caching purposes */ public static void loadImageWithCharacterAvatar( Context context, HostManager hostManager, String imageUrl, String stringAvatar, ImageView imageView, int imageWidth, int imageHeight) { CharacterDrawable avatarDrawable = getCharacterAvatar(context, stringAvatar); if (TextUtils.isEmpty(imageUrl)) { imageView.setImageDrawable(avatarDrawable); return; } if ((imageWidth) > 0 && (imageHeight > 0)) { hostManager.getPicasso() .load(hostManager.getHostInfo().getImageUrl(imageUrl)) .placeholder(avatarDrawable) .resize(imageWidth, imageHeight) .centerCrop() .into(imageView); } else { hostManager.getPicasso() .load(hostManager.getHostInfo().getImageUrl(imageUrl)) .fit() .centerCrop() .into(imageView); } } /** * Returns a CharacterDrawable that is suitable to use as an avatar * @param context Context * @param str String to use to create the avatar * @return Character avatar to use in a image view */ public static CharacterDrawable getCharacterAvatar(Context context, String str) { // Load character avatar if (characterAvatarColors == null) { characterAvatarColors = context .getResources() .obtainTypedArray(R.array.character_avatar_colors); } char charAvatar = TextUtils.isEmpty(str) ? ' ' : str.charAt(0); int avatarColorsIdx = TextUtils.isEmpty(str) ? 0 : Math.max(Character.getNumericValue(str.charAt(0)) + Character.getNumericValue(str.charAt(str.length() - 1)) + str.length(), 0) % characterAvatarColors.length(); int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000); // avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length()); return new CharacterDrawable(charAvatar, color); } public static boolean playPauseIconsLoaded = false; static int iconPauseResId = R.drawable.ic_pause_white_24dp, iconPlayResId = R.drawable.ic_play_arrow_white_24dp; /** * Sets play/pause button icon on a ImageView, based on speed * @param context Activity * @param view ImageView/ImageButton * @param speed Current player speed */ public static void setPlayPauseButtonIcon(Context context, ImageView view, int speed) { if (!playPauseIconsLoaded) { TypedArray styledAttributes = context.obtainStyledAttributes(new int[]{R.attr.iconPause, R.attr.iconPlay}); iconPauseResId = styledAttributes.getResourceId(styledAttributes.getIndex(0), R.drawable.ic_pause_white_24dp); iconPlayResId = styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_play_arrow_white_24dp); styledAttributes.recycle(); playPauseIconsLoaded = true; } view.setImageResource((speed == 1) ? iconPauseResId: iconPlayResId); } /** * Fills the standard cast info list, consisting of a {@link android.widget.GridLayout} * with actor images and a Textview with the name and the role of the additional cast. * The number of actor presented on the {@link android.widget.GridLayout} is controlled * through the global setting, and only actors with images are presented. * The rest are presented in the additionalCastView TextView * * @param activity Activity * @param castList Cast list * @param castListView GridLayout on which too show actors that have images */ public static void setupCastInfo(final Activity activity, List castList, GridLayout castListView, final Intent allCastActivityLaunchIntent) { HostManager hostManager = HostManager.getInstance(activity); Resources resources = activity.getResources(); DisplayMetrics displayMetrics = new DisplayMetrics(); WindowManager windowManager = (WindowManager)activity.getSystemService(Context.WINDOW_SERVICE); windowManager.getDefaultDisplay().getMetrics(displayMetrics); View.OnClickListener castListClickListener = new View.OnClickListener() { @Override public void onClick(View v) { Utils.openImdbForPerson(activity, (String)v.getTag()); } }; castListView.removeAllViews(); int numColumns = castListView.getColumnCount(); int numRows = resources.getInteger(R.integer.cast_grid_view_rows); int maxCastPictures = numColumns * numRows; int layoutMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.remote_content_hmargin); int imageMarginPx = 2 * resources.getDimensionPixelSize(R.dimen.image_grid_margin); int imageWidth = (displayMetrics.widthPixels - layoutMarginPx - numColumns * imageMarginPx) / numColumns; int imageHeight = (int)(imageWidth * 1.5); for (int i = 0; i < Math.min(castList.size(), maxCastPictures); i++) { VideoType.Cast actor = castList.get(i); View castView = LayoutInflater.from(activity).inflate(R.layout.grid_item_cast, castListView, false); ImageView castPicture = (ImageView) castView.findViewById(R.id.picture); TextView castName = (TextView) castView.findViewById(R.id.name); TextView castRole = (TextView) castView.findViewById(R.id.role); castView.getLayoutParams().width = imageWidth; castView.getLayoutParams().height = imageHeight; castView.setTag(actor.name); UIUtils.loadImageWithCharacterAvatar(activity, hostManager, actor.thumbnail, actor.name, castPicture, imageWidth, imageHeight); if ((i == maxCastPictures - 1) && (castList.size() > i + 1)) { View castNameGroup = castView.findViewById(R.id.cast_name_group); View allCastGroup = castView.findViewById(R.id.all_cast_group); TextView remainingCastCount = (TextView)castView.findViewById(R.id.remaining_cast_count); castNameGroup.setVisibility(View.GONE); allCastGroup.setVisibility(View.VISIBLE); remainingCastCount.setText(String.format(activity.getString(R.string.remaining_cast_count), castList.size() - maxCastPictures + 1)); castView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { activity.startActivity(allCastActivityLaunchIntent); activity.overridePendingTransition(R.anim.activity_in, R.anim.activity_out); } }); } else { castName.setText(actor.name); castRole.setText(actor.role); castView.setOnClickListener(castListClickListener); } castListView.addView(castView); } } /** * Simple wrapper to {@link NetUtils#sendWolMagicPacket(String, String, int)} * that sends a WoL magic packet in a new thread * * @param context Context * @param hostInfo Host to send WoL */ public static void sendWolAsync(Context context, final HostInfo hostInfo) { if (hostInfo == null) return; // Send WoL magic packet on a new thread new Thread(new Runnable() { @Override public void run() { NetUtils.sendWolMagicPacket(hostInfo.getMacAddress(), hostInfo.getAddress(), hostInfo.getWolPort()); } }).start(); Toast.makeText(context, R.string.wol_sent, Toast.LENGTH_SHORT).show(); } // /** // * Sets the default {@link android.support.v4.widget.SwipeRefreshLayout} color scheme // * @param swipeRefreshLayout layout // */ // public static void setSwipeRefreshLayoutColorScheme(SwipeRefreshLayout swipeRefreshLayout) { // Resources.Theme theme = swipeRefreshLayout.getContext().getTheme(); // TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { // R.attr.refreshColor1, // R.attr.refreshColor2, // R.attr.refreshColor3, // R.attr.refreshColor4, // }); // // swipeRefreshLayout.setColorScheme(styledAttributes.getResourceId(0, android.R.color.holo_blue_dark), // styledAttributes.getResourceId(1, android.R.color.holo_purple), // styledAttributes.getResourceId(2, android.R.color.holo_red_dark), // styledAttributes.getResourceId(3, android.R.color.holo_green_dark)); // styledAttributes.recycle(); // } // /** // * Sets a views padding top/bottom to account for the system bars // * (Top status and action bar, bottom nav bar, right nav bar if in ladscape mode) // * // * @param context Context // * @param view View to pad // * @param padTop Whether to set views paddingTop // * @param padRight Whether to set views paddingRight (for nav bar in landscape mode) // * @param padBottom Whether to set views paddingBottom // */ // public static void setPaddingForSystemBars(Activity context, View view, // boolean padTop, boolean padRight, boolean padBottom) { // //if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return; // SystemBarTintManager tintManager = new SystemBarTintManager(context); // SystemBarTintManager.SystemBarConfig config = tintManager.getConfig(); // // view.setPadding(view.getPaddingLeft(), // padTop ? config.getPixelInsetTop(true) : view.getPaddingTop(), // padRight? config.getPixelInsetRight() : view.getPaddingRight(), // padBottom ? config.getPixelInsetBottom() : view.getPaddingBottom()); // } /** * Returns a theme resource Id given the value stored in Shared Preferences * @param prefThemeValue Shared Preferences value for the theme * @return Android resource id of the theme */ public static int getThemeResourceId(String prefThemeValue) { switch (Integer.valueOf(prefThemeValue)) { case 0: return R.style.NightTheme; case 1: return R.style.DayTheme; case 2: return R.style.MistTheme; case 3: return R.style.SolarizedLightTheme; case 4: return R.style.SolarizedDarkTheme; default: return R.style.NightTheme; } } public static void handleVibration(Context context) { if(context == null) return; Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (!vibrator.hasVibrator()) return; //Check if we should vibrate boolean vibrateOnPress = PreferenceManager .getDefaultSharedPreferences(context) .getBoolean(Settings.KEY_PREF_VIBRATE_REMOTE_BUTTONS, Settings.DEFAULT_PREF_VIBRATE_REMOTE_BUTTONS); if (vibrateOnPress) { vibrator.vibrate(UIUtils.buttonVibrationDuration); } } /** * Launches the remote activity, performing a circular reveal animation if * Lollipop or later * * @param context Context * @param centerX Center X of the animation * @param centerY Center Y of the animation * @param exitTransitionView View to reveal. Should occupy the whole screen and * be invisible before calling this */ @TargetApi(21) public static void switchToRemoteWithAnimation(final Context context, int centerX, int centerY, final View exitTransitionView) { final Intent launchIntent = new Intent(context, RemoteActivity.class); if (Utils.isLollipopOrLater()) { // Show the animation int endRadius = Math.max(exitTransitionView.getHeight(), exitTransitionView.getWidth()); Animator exitAnim = ViewAnimationUtils.createCircularReveal(exitTransitionView, centerX, centerY, 0, endRadius); exitAnim.setDuration(200); exitAnim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { // Launch remote activity context.startActivity(launchIntent); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); exitTransitionView.setVisibility(View.VISIBLE); exitAnim.start(); } else { // No animation show, just launch the remote context.startActivity(launchIntent); } } /** * Use this to manually start the swiperefreshlayout refresh animation. * Fixes issue with refresh animation not showing when using appcompat library (from version 20?) * See https://code.google.com/p/android/issues/detail?id=77712 * @param layout */ public static void showRefreshAnimation(@NonNull final SwipeRefreshLayout layout) { layout.post(new Runnable() { @Override public void run() { layout.setRefreshing(true); } }); } /** * Returns true if {@param view} is visible within {@param container}'s bounds. */ public static boolean isViewInBounds(@NonNull View container, @NonNull View view) { Rect containerBounds = new Rect(); container.getHitRect(containerBounds); return view.getLocalVisibleRect(containerBounds); } /** * Downloads a list of songs. Asks the user for confirmation if one or more songs * already exist on the device * @param context required to show the user a dialog * @param songInfoList the song infos for the songs that need to be downloaded * @param hostInfo the host info from which the songs should be downloaded * @param callbackHandler Thread handler that should be used to handle the download result */ public static void downloadSongs(final Context context, final ArrayList songInfoList, final HostInfo hostInfo, final Handler callbackHandler) { if (songInfoList == null || songInfoList.size() == 0) { Toast.makeText(context, R.string.no_songs_to_download, Toast.LENGTH_LONG); return; } // Check if any file exists and whether to overwrite it boolean someFilesExist = false; for (FileDownloadHelper.SongInfo songInfo : songInfoList) { File file = new File(songInfo.getAbsoluteFilePath()); if (file.exists()) { someFilesExist = true; break; } } if (someFilesExist) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.download) .setMessage(songInfoList.size() > 1 ? R.string.download_files_exists : R.string.download_file_exists) .setPositiveButton(R.string.overwrite, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { FileDownloadHelper.downloadFiles(context, hostInfo, songInfoList, FileDownloadHelper.OVERWRITE_FILES, callbackHandler); } }) .setNeutralButton(R.string.download_with_new_name, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { FileDownloadHelper.downloadFiles(context, hostInfo, songInfoList, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, callbackHandler); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }) .show(); } else { if ( songInfoList.size() > 12 ) { // No scientific reason this should be 12. I just happen to like 12. String message = context.getResources().getString(R.string.confirm_songs_download); AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.download) .setMessage(String.format(message, songInfoList.size())) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { FileDownloadHelper.downloadFiles(context, hostInfo, songInfoList, FileDownloadHelper.OVERWRITE_FILES, callbackHandler); } }) .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }) .show(); } else { FileDownloadHelper.downloadFiles(context, hostInfo, songInfoList, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, callbackHandler); } } } /** * Highlights an image view * @param context * @param view * @param highlight true if the image view should be highlighted, false otherwise */ public static void highlightImageView(Context context, ImageView view, boolean highlight) { if (highlight) { Resources.Theme theme = context.getTheme(); TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ R.attr.colorAccent}); view.setColorFilter( styledAttributes.getColor(styledAttributes.getIndex(0), context.getResources().getColor(R.color.accent_default))); styledAttributes.recycle(); } else { view.clearColorFilter(); } } }