/* * Copyright (C) 2015 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.android.libraries.cast.companionlibrary.utils; import static com.google.android.libraries.cast.companionlibrary.utils.LogUtils.LOGE; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaTrack; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GooglePlayServicesUtil; import com.google.android.gms.common.images.WebImage; import android.app.Activity; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.RectF; import android.net.Uri; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; import android.os.Looper; import android.os.Parcelable; import android.text.TextUtils; import android.text.format.DateUtils; import android.util.TypedValue; import android.view.Display; import android.view.WindowManager; import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.util.ArrayList; import java.util.Calendar; import java.util.List; /** * A collection of utility methods, all static. */ public final class Utils { private static final String TAG = LogUtils.makeLogTag(Utils.class); private static final String KEY_MEDIA_TYPE = "media-type"; private static final String KEY_IMAGES = "images"; private static final String KEY_URL = "movie-urls"; private static final String KEY_CONTENT_TYPE = "content-type"; private static final String KEY_STREAM_TYPE = "stream-type"; private static final String KEY_CUSTOM_DATA = "custom-data"; private static final String KEY_STREAM_DURATION = "stream-duration"; private static final String KEY_TRACK_ID = "track-id"; private static final String KEY_TRACK_CONTENT_ID = "track-custom-id"; private static final String KEY_TRACK_NAME = "track-name"; private static final String KEY_TRACK_TYPE = "track-type"; private static final String KEY_TRACK_CONTENT_TYPE = "track-content-type"; private static final String KEY_TRACK_SUBTYPE = "track-subtype"; private static final String KEY_TRACK_LANGUAGE = "track-language"; private static final String KEY_TRACK_CUSTOM_DATA = "track-custom-data"; private static final String KEY_TRACKS_DATA = "track-data"; public static final boolean IS_KITKAT_OR_ABOVE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; private Utils() { } /** * Formats time from milliseconds to hh:mm:ss string format. */ public static String formatMillis(int millis) { return DateUtils.formatElapsedTime(millis/1000); } /** * Shows a (long) toast. */ public static void showToast(Context context, int resourceId) { Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show(); } /** * Returns the URL of an image for the {@link MediaInfo} at the given index. Index should be a * number between 0 and {@code n-1} where {@code n} is the number of images for that given item. */ public static String getImageUrl(MediaInfo info, int index) { Uri uri = getImageUri(info, index); if (uri != null) { return uri.toString(); } return null; } /** * Returns the {@code Uri} address of an image for the {@link MediaInfo} at the given * index. Index should be a number between 0 and {@code n - 1} where {@code n} is the * number of images for that given item. */ public static Uri getImageUri(MediaInfo info, int index) { MediaMetadata mediaMetadata = info.getMetadata(); if (mediaMetadata != null && mediaMetadata.getImages().size() > index) { return mediaMetadata.getImages().get(index).getUrl(); } return null; } /** * A utility method to validate that the appropriate version of the Google Play Services is * available on the device. If not, it will open a dialog to address the issue. The dialog * displays a localized message about the error and upon user confirmation (by tapping on * dialog) will direct them to the Play Store if Google Play services is out of date or * missing, or to system settings if Google Play services is disabled on the device. */ public static boolean checkGooglePlayServices(final Activity activity) { final int googlePlayServicesCheck = GooglePlayServicesUtil.isGooglePlayServicesAvailable( activity); switch (googlePlayServicesCheck) { case ConnectionResult.SUCCESS: return true; default: Dialog dialog = GooglePlayServicesUtil.getErrorDialog(googlePlayServicesCheck, activity, 0); dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialogInterface) { activity.finish(); } }); dialog.show(); } return false; } /** * Builds and returns a {@link Bundle} which contains a select subset of data in the * {@link MediaInfo}. Since {@link MediaInfo} is not {@link Parcelable}, one can use this * container bundle to pass around from one activity to another. * * @see <code>bundleToMediaInfo()</code> */ public static Bundle mediaInfoToBundle(MediaInfo info) { if (info == null) { return null; } MediaMetadata md = info.getMetadata(); Bundle wrapper = new Bundle(); wrapper.putString(MediaMetadata.KEY_TITLE, md.getString(MediaMetadata.KEY_TITLE)); wrapper.putString(MediaMetadata.KEY_SUBTITLE, md.getString(MediaMetadata.KEY_SUBTITLE)); wrapper.putString(MediaMetadata.KEY_ALBUM_TITLE, md.getString(MediaMetadata.KEY_ALBUM_TITLE)); wrapper.putString(MediaMetadata.KEY_ALBUM_ARTIST, md.getString(MediaMetadata.KEY_ALBUM_ARTIST)); wrapper.putString(MediaMetadata.KEY_COMPOSER, md.getString(MediaMetadata.KEY_COMPOSER)); wrapper.putString(MediaMetadata.KEY_SERIES_TITLE, md.getString(MediaMetadata.KEY_SERIES_TITLE)); wrapper.putInt(MediaMetadata.KEY_SEASON_NUMBER, md.getInt(MediaMetadata.KEY_SEASON_NUMBER)); wrapper.putInt(MediaMetadata.KEY_EPISODE_NUMBER, md.getInt(MediaMetadata.KEY_EPISODE_NUMBER)); Calendar releaseCalendar = md.getDate(MediaMetadata.KEY_RELEASE_DATE); if (releaseCalendar != null) { long releaseMillis = releaseCalendar.getTimeInMillis(); wrapper.putLong(MediaMetadata.KEY_RELEASE_DATE, releaseMillis); } wrapper.putInt(KEY_MEDIA_TYPE, info.getMetadata().getMediaType()); wrapper.putString(KEY_URL, info.getContentId()); wrapper.putString(MediaMetadata.KEY_STUDIO, md.getString(MediaMetadata.KEY_STUDIO)); wrapper.putString(KEY_CONTENT_TYPE, info.getContentType()); wrapper.putInt(KEY_STREAM_TYPE, info.getStreamType()); wrapper.putLong(KEY_STREAM_DURATION, info.getStreamDuration()); if (!md.getImages().isEmpty()) { ArrayList<String> urls = new ArrayList<>(); for (WebImage img : md.getImages()) { urls.add(img.getUrl().toString()); } wrapper.putStringArrayList(KEY_IMAGES, urls); } JSONObject customData = info.getCustomData(); if (customData != null) { wrapper.putString(KEY_CUSTOM_DATA, customData.toString()); } if (info.getMediaTracks() != null && !info.getMediaTracks().isEmpty()) { try { JSONArray jsonArray = new JSONArray(); for (MediaTrack mt : info.getMediaTracks()) { JSONObject jsonObject = new JSONObject(); jsonObject.put(KEY_TRACK_NAME, mt.getName()); jsonObject.put(KEY_TRACK_CONTENT_ID, mt.getContentId()); jsonObject.put(KEY_TRACK_ID, mt.getId()); jsonObject.put(KEY_TRACK_LANGUAGE, mt.getLanguage()); jsonObject.put(KEY_TRACK_TYPE, mt.getType()); jsonObject.put(KEY_TRACK_CONTENT_TYPE, mt.getContentType()); if (mt.getSubtype() != MediaTrack.SUBTYPE_UNKNOWN) { jsonObject.put(KEY_TRACK_SUBTYPE, mt.getSubtype()); } if (mt.getCustomData() != null) { jsonObject.put(KEY_TRACK_CUSTOM_DATA, mt.getCustomData().toString()); } jsonArray.put(jsonObject); } wrapper.putString(KEY_TRACKS_DATA, jsonArray.toString()); } catch (JSONException e) { LOGE(TAG, "mediaInfoToBundle(): Failed to convert Tracks data to json", e); } } return wrapper; } /** * Builds and returns a {@link MediaInfo} that was wrapped in a {@link Bundle} by * <code>mediaInfoToBundle</code>. It is assumed that the type of the {@link MediaInfo} is * {@code MediaMetaData.MEDIA_TYPE_MOVIE} * * @see <code>mediaInfoToBundle()</code> */ public static MediaInfo bundleToMediaInfo(Bundle wrapper) { if (wrapper == null) { return null; } MediaMetadata metaData = new MediaMetadata(wrapper.getInt(KEY_MEDIA_TYPE)); metaData.putString(MediaMetadata.KEY_SUBTITLE, wrapper.getString(MediaMetadata.KEY_SUBTITLE)); metaData.putString(MediaMetadata.KEY_TITLE, wrapper.getString(MediaMetadata.KEY_TITLE)); metaData.putString(MediaMetadata.KEY_STUDIO, wrapper.getString(MediaMetadata.KEY_STUDIO)); metaData.putString(MediaMetadata.KEY_ALBUM_ARTIST, wrapper.getString(MediaMetadata.KEY_ALBUM_ARTIST)); metaData.putString(MediaMetadata.KEY_ALBUM_TITLE, wrapper.getString(MediaMetadata.KEY_ALBUM_TITLE)); metaData.putString(MediaMetadata.KEY_COMPOSER, wrapper.getString(MediaMetadata.KEY_COMPOSER)); metaData.putString(MediaMetadata.KEY_SERIES_TITLE, wrapper.getString(MediaMetadata.KEY_SERIES_TITLE)); metaData.putInt(MediaMetadata.KEY_SEASON_NUMBER, wrapper.getInt(MediaMetadata.KEY_SEASON_NUMBER)); metaData.putInt(MediaMetadata.KEY_EPISODE_NUMBER, wrapper.getInt(MediaMetadata.KEY_EPISODE_NUMBER)); long releaseDateMillis = wrapper.getLong(MediaMetadata.KEY_RELEASE_DATE, 0); if (releaseDateMillis > 0) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(releaseDateMillis); metaData.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar); } ArrayList<String> images = wrapper.getStringArrayList(KEY_IMAGES); if (images != null && !images.isEmpty()) { for (String url : images) { Uri uri = Uri.parse(url); metaData.addImage(new WebImage(uri)); } } String customDataStr = wrapper.getString(KEY_CUSTOM_DATA); JSONObject customData = null; if (!TextUtils.isEmpty(customDataStr)) { try { customData = new JSONObject(customDataStr); } catch (JSONException e) { LOGE(TAG, "Failed to deserialize the custom data string: custom data= " + customDataStr); } } List<MediaTrack> mediaTracks = null; if (wrapper.getString(KEY_TRACKS_DATA) != null) { try { JSONArray jsonArray = new JSONArray(wrapper.getString(KEY_TRACKS_DATA)); mediaTracks = new ArrayList<>(); if (jsonArray.length() > 0) { for (int i = 0; i < jsonArray.length(); i++) { JSONObject jsonObj = (JSONObject) jsonArray.get(i); MediaTrack.Builder builder = new MediaTrack.Builder( jsonObj.getLong(KEY_TRACK_ID), jsonObj.getInt(KEY_TRACK_TYPE)); if (jsonObj.has(KEY_TRACK_NAME)) { builder.setName(jsonObj.getString(KEY_TRACK_NAME)); } if (jsonObj.has(KEY_TRACK_SUBTYPE)) { builder.setSubtype(jsonObj.getInt(KEY_TRACK_SUBTYPE)); } if (jsonObj.has(KEY_TRACK_CONTENT_ID)) { builder.setContentId(jsonObj.getString(KEY_TRACK_CONTENT_ID)); } if (jsonObj.has(KEY_TRACK_CONTENT_TYPE)) { builder.setContentType(jsonObj.getString(KEY_TRACK_CONTENT_TYPE)); } if (jsonObj.has(KEY_TRACK_LANGUAGE)) { builder.setLanguage(jsonObj.getString(KEY_TRACK_LANGUAGE)); } if (jsonObj.has(KEY_TRACKS_DATA)) { builder.setCustomData( new JSONObject(jsonObj.getString(KEY_TRACKS_DATA))); } mediaTracks.add(builder.build()); } } } catch (JSONException e) { LOGE(TAG, "Failed to build media tracks from the wrapper bundle", e); } } MediaInfo.Builder mediaBuilder = new MediaInfo.Builder(wrapper.getString(KEY_URL)) .setStreamType(wrapper.getInt(KEY_STREAM_TYPE)) .setContentType(wrapper.getString(KEY_CONTENT_TYPE)) .setMetadata(metaData) .setCustomData(customData) .setMediaTracks(mediaTracks); if (wrapper.containsKey(KEY_STREAM_DURATION) && wrapper.getLong(KEY_STREAM_DURATION) >= 0) { mediaBuilder.setStreamDuration(wrapper.getLong(KEY_STREAM_DURATION)); } return mediaBuilder.build(); } /** * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi. */ public static String getWifiSsid(Context context) { WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); WifiInfo wifiInfo = wifiManager.getConnectionInfo(); if (wifiInfo != null) { return wifiInfo.getSSID(); } return null; } /** * Scale and center-crop a bitmap to fit the given dimensions. */ public static Bitmap scaleAndCenterCropBitmap(Bitmap source, int newHeight, int newWidth) { if (source == null) { return null; } int sourceWidth = source.getWidth(); int sourceHeight = source.getHeight(); float xScale = (float) newWidth / sourceWidth; float yScale = (float) newHeight / sourceHeight; float scale = Math.max(xScale, yScale); float scaledWidth = scale * sourceWidth; float scaledHeight = scale * sourceHeight; float left = (newWidth - scaledWidth) / 2; float top = (newHeight - scaledHeight) / 2; RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight); Bitmap.Config config = source.getConfig() == null ? Bitmap.Config.ARGB_8888 : source.getConfig(); Bitmap destination = Bitmap.createBitmap(newWidth, newHeight, config); Canvas canvas = new Canvas(destination); canvas.drawBitmap(source, null, targetRect, null); return destination; } /** * Converts DIP (or DP) to Pixels */ public static int convertDpToPixel(Context context, float dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } /** * Given a list of queue items, this method recreates an identical list of items except that the * {@code itemId} of each item is erased, in effect preparing the list to be reloaded on the * receiver. */ public static MediaQueueItem[] rebuildQueue(List<MediaQueueItem> items) { if (items == null || items.isEmpty()) { return null; } MediaQueueItem[] rebuiltQueue = new MediaQueueItem[items.size()]; for (int i = 0; i < items.size(); i++) { rebuiltQueue[i] = rebuildQueueItem(items.get(i)); } return rebuiltQueue; } /** * Given a list of queue items, and a new item, this method recreates an identical list of items * from the queue, except that the {@code itemId} of each item is erased, in effect preparing * the list to be reloaded. Then, it appends the new item to teh end of the rebuilt list and * returns the result. */ public static MediaQueueItem[] rebuildQueueAndAppend(List<MediaQueueItem> items, MediaQueueItem currentItem) { if (items == null || items.isEmpty()) { return new MediaQueueItem[]{currentItem}; } MediaQueueItem[] rebuiltQueue = new MediaQueueItem[items.size() + 1]; for (int i = 0; i < items.size(); i++) { rebuiltQueue[i] = rebuildQueueItem(items.get(i)); } rebuiltQueue[items.size()] = currentItem; return rebuiltQueue; } /** * Given a queue item, it returns an identical item except that the {@code itemId} has been * cleared. */ public static MediaQueueItem rebuildQueueItem(MediaQueueItem item) { return new MediaQueueItem.Builder(item).clearItemId().build(); } /** * Returns {@code true} if and only if the current thread is the UI thread. */ public static boolean isUiThread() { return Looper.getMainLooper().equals(Looper.myLooper()); } /** * Asserts that the current thread is the UI thread; if not, this method throws an * {@link IllegalStateException}. */ public static void assertUiThread() { if (!isUiThread()) { throw new IllegalStateException("Not a UI thread"); } } /** * Asserts that the current thread is a worker (i.e. non-UI) thread; if not, this * method throws an {@link IllegalStateException}. */ public static void assertNonUiThread() { if (isUiThread()) { throw new IllegalStateException("Not a non-UI thread"); } } /** * Returns the {@code object} if it is not {@code null}, or throws a * {@link NullPointerException} otherwise. * * @param object The object to inspect * @param name A name for the object to be used in the NPE message */ public static <T> T assertNotNull(T object, String name) { if (object == null) { throw new NullPointerException(name + " cannot be null"); } return object; } /** * Asserts that the {@code string} is not empty or {@code null}. It throws an * {@link IllegalArgumentException} if it is, otherwise returns the original string. * * @param string The string to inspect * @param name A name for the string to be used in the NPE message */ public static String assertNotEmpty(String string, String name) { if (TextUtils.isEmpty(string)) { throw new IllegalArgumentException(name + " cannot be null or empty"); } return string; } // Display.getHeight() and getWidth() are deprecated but Display.getSize(), which is now the // recommended replacement was introduced in API level 13+. @SuppressWarnings("deprecation") /** * Returns the screen/display size. */ public static Point getDisplaySize(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR2) { return new Point(display.getWidth(), display.getHeight()); } else { Point outSize = new Point(); display.getSize(outSize); return outSize; } } /** * Returns {@code true} if and only if the {@code tracks} include at least one audio or text * track. */ public static boolean hasAudioOrTextTrack(List<MediaTrack> tracks) { if (tracks == null || tracks.isEmpty()) { return false; } for (MediaTrack track : tracks) { if (track.getType() == MediaTrack.TYPE_AUDIO || track.getType() == MediaTrack.TYPE_TEXT) { return true; } } return false; } }