| /* |
| * Copyright (C) 2017 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. |
| */ |
| |
| package com.android.tv.data; |
| |
| import android.annotation.TargetApi; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.SQLException; |
| import android.graphics.Bitmap; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.media.tv.TvContract; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Build; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.MainThread; |
| import android.support.media.tv.ChannelLogoUtils; |
| import android.support.media.tv.PreviewProgram; |
| import android.util.Log; |
| import android.util.Pair; |
| import com.android.tv.R; |
| import com.android.tv.common.util.PermissionUtils; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| |
| /** Class to manage the preview data. */ |
| @TargetApi(Build.VERSION_CODES.O) |
| @MainThread |
| public class PreviewDataManager { |
| private static final String TAG = "PreviewDataManager"; |
| private static final boolean DEBUG = false; |
| |
| /** Invalid preview channel ID. */ |
| public static final long INVALID_PREVIEW_CHANNEL_ID = -1; |
| |
| @IntDef({TYPE_DEFAULT_PREVIEW_CHANNEL, TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface PreviewChannelType {} |
| |
| /** Type of default preview channel */ |
| public static final int TYPE_DEFAULT_PREVIEW_CHANNEL = 1; |
| /** Type of recorded program channel */ |
| public static final int TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL = 2; |
| |
| private final Context mContext; |
| private final ContentResolver mContentResolver; |
| private boolean mLoadFinished; |
| private PreviewData mPreviewData = new PreviewData(); |
| private final Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>(); |
| |
| private QueryPreviewDataTask mQueryPreviewTask; |
| private final Map<Long, CreatePreviewChannelTask> mCreatePreviewChannelTasks = new HashMap<>(); |
| private final Map<Long, UpdatePreviewProgramTask> mUpdatePreviewProgramTasks = new HashMap<>(); |
| |
| private final int mPreviewChannelLogoWidth; |
| private final int mPreviewChannelLogoHeight; |
| |
| public PreviewDataManager(Context context) { |
| mContext = context.getApplicationContext(); |
| mContentResolver = context.getContentResolver(); |
| mPreviewChannelLogoWidth = |
| mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_width); |
| mPreviewChannelLogoHeight = |
| mContext.getResources().getDimensionPixelSize(R.dimen.preview_channel_logo_height); |
| } |
| |
| /** Starts the preview data manager. */ |
| public void start() { |
| if (mQueryPreviewTask == null) { |
| mQueryPreviewTask = new QueryPreviewDataTask(); |
| mQueryPreviewTask.execute(); |
| } |
| } |
| |
| /** Stops the preview data manager. */ |
| public void stop() { |
| if (mQueryPreviewTask != null) { |
| mQueryPreviewTask.cancel(true); |
| } |
| for (CreatePreviewChannelTask createPreviewChannelTask : |
| mCreatePreviewChannelTasks.values()) { |
| createPreviewChannelTask.cancel(true); |
| } |
| for (UpdatePreviewProgramTask updatePreviewProgramTask : |
| mUpdatePreviewProgramTasks.values()) { |
| updatePreviewProgramTask.cancel(true); |
| } |
| |
| mQueryPreviewTask = null; |
| mCreatePreviewChannelTasks.clear(); |
| mUpdatePreviewProgramTasks.clear(); |
| } |
| |
| /** Gets preview channel ID from the preview channel type. */ |
| public @PreviewChannelType long getPreviewChannelId(long previewChannelType) { |
| return mPreviewData.getPreviewChannelId(previewChannelType); |
| } |
| |
| /** Creates default preview channel. */ |
| public void createDefaultPreviewChannel( |
| OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) { |
| createPreviewChannel(TYPE_DEFAULT_PREVIEW_CHANNEL, onPreviewChannelCreationResultListener); |
| } |
| |
| /** Creates a preview channel for specific channel type. */ |
| public void createPreviewChannel( |
| @PreviewChannelType long previewChannelType, |
| OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) { |
| CreatePreviewChannelTask currentRunningCreateTask = |
| mCreatePreviewChannelTasks.get(previewChannelType); |
| if (currentRunningCreateTask == null) { |
| CreatePreviewChannelTask createPreviewChannelTask = |
| new CreatePreviewChannelTask(previewChannelType); |
| createPreviewChannelTask.addOnPreviewChannelCreationResultListener( |
| onPreviewChannelCreationResultListener); |
| createPreviewChannelTask.execute(); |
| mCreatePreviewChannelTasks.put(previewChannelType, createPreviewChannelTask); |
| } else { |
| currentRunningCreateTask.addOnPreviewChannelCreationResultListener( |
| onPreviewChannelCreationResultListener); |
| } |
| } |
| |
| /** Returns {@code true} if the preview data is loaded. */ |
| public boolean isLoadFinished() { |
| return mLoadFinished; |
| } |
| |
| /** Adds listener. */ |
| public void addListener(PreviewDataListener previewDataListener) { |
| mPreviewDataListeners.add(previewDataListener); |
| } |
| |
| /** Removes listener. */ |
| public void removeListener(PreviewDataListener previewDataListener) { |
| mPreviewDataListeners.remove(previewDataListener); |
| } |
| |
| /** Updates the preview programs table for a specific preview channel. */ |
| public void updatePreviewProgramsForChannel( |
| long previewChannelId, |
| Set<PreviewProgramContent> programs, |
| PreviewDataListener previewDataListener) { |
| UpdatePreviewProgramTask currentRunningUpdateTask = |
| mUpdatePreviewProgramTasks.get(previewChannelId); |
| if (currentRunningUpdateTask != null |
| && currentRunningUpdateTask.getPrograms().equals(programs)) { |
| currentRunningUpdateTask.addPreviewDataListener(previewDataListener); |
| return; |
| } |
| UpdatePreviewProgramTask updatePreviewProgramTask = |
| new UpdatePreviewProgramTask(previewChannelId, programs); |
| updatePreviewProgramTask.addPreviewDataListener(previewDataListener); |
| if (currentRunningUpdateTask != null) { |
| currentRunningUpdateTask.cancel(true); |
| currentRunningUpdateTask.saveStatus(); |
| updatePreviewProgramTask.addPreviewDataListeners( |
| currentRunningUpdateTask.getPreviewDataListeners()); |
| } |
| updatePreviewProgramTask.execute(); |
| mUpdatePreviewProgramTasks.put(previewChannelId, updatePreviewProgramTask); |
| } |
| |
| private void notifyPreviewDataLoadFinished() { |
| for (PreviewDataListener l : mPreviewDataListeners) { |
| l.onPreviewDataLoadFinished(); |
| } |
| } |
| |
| public interface PreviewDataListener { |
| /** Called when the preview data is loaded. */ |
| void onPreviewDataLoadFinished(); |
| |
| /** Called when the preview data is updated. */ |
| void onPreviewDataUpdateFinished(); |
| } |
| |
| public interface OnPreviewChannelCreationResultListener { |
| /** |
| * Called when the creation of preview channel is finished. |
| * |
| * @param createdPreviewChannelId The preview channel ID if created successfully, otherwise |
| * it's {@value #INVALID_PREVIEW_CHANNEL_ID}. |
| */ |
| void onPreviewChannelCreationResult(long createdPreviewChannelId); |
| } |
| |
| private final class QueryPreviewDataTask extends AsyncTask<Void, Void, PreviewData> { |
| private final String PARAM_PREVIEW = "preview"; |
| private final String mChannelSelection = TvContract.Channels.COLUMN_PACKAGE_NAME + "=?"; |
| |
| @Override |
| protected PreviewData doInBackground(Void... voids) { |
| // Query preview channels and programs. |
| if (DEBUG) Log.d(TAG, "QueryPreviewDataTask.doInBackground"); |
| PreviewData previewData = new PreviewData(); |
| try { |
| Uri previewChannelsUri = |
| PreviewDataUtils.addQueryParamToUri( |
| TvContract.Channels.CONTENT_URI, |
| new Pair<>(PARAM_PREVIEW, String.valueOf(true))); |
| String packageName = mContext.getPackageName(); |
| if (PermissionUtils.hasAccessAllEpg(mContext)) { |
| try (Cursor cursor = |
| mContentResolver.query( |
| previewChannelsUri, |
| android.support.media.tv.Channel.PROJECTION, |
| mChannelSelection, |
| new String[] {packageName}, |
| null)) { |
| if (cursor != null) { |
| while (cursor.moveToNext()) { |
| android.support.media.tv.Channel previewChannel = |
| android.support.media.tv.Channel.fromCursor(cursor); |
| Long previewChannelType = previewChannel.getInternalProviderFlag1(); |
| if (previewChannelType != null) { |
| previewData.addPreviewChannelId( |
| previewChannelType, previewChannel.getId()); |
| } |
| } |
| } |
| } |
| } else { |
| try (Cursor cursor = |
| mContentResolver.query( |
| previewChannelsUri, |
| android.support.media.tv.Channel.PROJECTION, |
| null, |
| null, |
| null)) { |
| if (cursor != null) { |
| while (cursor.moveToNext()) { |
| android.support.media.tv.Channel previewChannel = |
| android.support.media.tv.Channel.fromCursor(cursor); |
| Long previewChannelType = previewChannel.getInternalProviderFlag1(); |
| if (packageName.equals(previewChannel.getPackageName()) |
| && previewChannelType != null) { |
| previewData.addPreviewChannelId( |
| previewChannelType, previewChannel.getId()); |
| } |
| } |
| } |
| } |
| } |
| |
| for (long previewChannelId : previewData.getAllPreviewChannelIds().values()) { |
| Uri previewProgramsUriForPreviewChannel = |
| TvContract.buildPreviewProgramsUriForChannel(previewChannelId); |
| try (Cursor previewProgramCursor = |
| mContentResolver.query( |
| previewProgramsUriForPreviewChannel, |
| PreviewProgram.PROJECTION, |
| null, |
| null, |
| null)) { |
| if (previewProgramCursor != null) { |
| while (previewProgramCursor.moveToNext()) { |
| PreviewProgram previewProgram = |
| PreviewProgram.fromCursor(previewProgramCursor); |
| previewData.addPreviewProgram(previewProgram); |
| } |
| } |
| } |
| } |
| } catch (SQLException e) { |
| Log.w(TAG, "Unable to get preview data", e); |
| } |
| return previewData; |
| } |
| |
| @Override |
| protected void onPostExecute(PreviewData result) { |
| super.onPostExecute(result); |
| if (mQueryPreviewTask == this) { |
| mQueryPreviewTask = null; |
| mPreviewData = new PreviewData(result); |
| mLoadFinished = true; |
| notifyPreviewDataLoadFinished(); |
| } |
| } |
| } |
| |
| private final class CreatePreviewChannelTask extends AsyncTask<Void, Void, Long> { |
| private final long mPreviewChannelType; |
| private Set<OnPreviewChannelCreationResultListener> |
| mOnPreviewChannelCreationResultListeners = new CopyOnWriteArraySet<>(); |
| |
| public CreatePreviewChannelTask(long previewChannelType) { |
| mPreviewChannelType = previewChannelType; |
| } |
| |
| public void addOnPreviewChannelCreationResultListener( |
| OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener) { |
| if (onPreviewChannelCreationResultListener != null) { |
| mOnPreviewChannelCreationResultListeners.add( |
| onPreviewChannelCreationResultListener); |
| } |
| } |
| |
| @Override |
| protected Long doInBackground(Void... params) { |
| if (DEBUG) Log.d(TAG, "CreatePreviewChannelTask.doInBackground"); |
| long previewChannelId; |
| try { |
| Uri channelUri = |
| mContentResolver.insert( |
| TvContract.Channels.CONTENT_URI, |
| PreviewDataUtils.createPreviewChannel(mContext, mPreviewChannelType) |
| .toContentValues()); |
| if (channelUri != null) { |
| previewChannelId = ContentUris.parseId(channelUri); |
| } else { |
| Log.e(TAG, "Fail to insert preview channel"); |
| return INVALID_PREVIEW_CHANNEL_ID; |
| } |
| } catch (UnsupportedOperationException | NumberFormatException e) { |
| Log.e(TAG, "Fail to get channel ID"); |
| return INVALID_PREVIEW_CHANNEL_ID; |
| } |
| Drawable appIcon = mContext.getApplicationInfo().loadIcon(mContext.getPackageManager()); |
| if (appIcon != null && appIcon instanceof BitmapDrawable) { |
| ChannelLogoUtils.storeChannelLogo( |
| mContext, |
| previewChannelId, |
| Bitmap.createScaledBitmap( |
| ((BitmapDrawable) appIcon).getBitmap(), |
| mPreviewChannelLogoWidth, |
| mPreviewChannelLogoHeight, |
| false)); |
| } |
| return previewChannelId; |
| } |
| |
| @Override |
| protected void onPostExecute(Long result) { |
| super.onPostExecute(result); |
| if (result != INVALID_PREVIEW_CHANNEL_ID) { |
| mPreviewData.addPreviewChannelId(mPreviewChannelType, result); |
| } |
| for (OnPreviewChannelCreationResultListener onPreviewChannelCreationResultListener : |
| mOnPreviewChannelCreationResultListeners) { |
| onPreviewChannelCreationResultListener.onPreviewChannelCreationResult(result); |
| } |
| mCreatePreviewChannelTasks.remove(mPreviewChannelType); |
| } |
| } |
| |
| /** |
| * Updates the whole data which belongs to the package in preview programs table for a specific |
| * preview channel with a set of {@link PreviewProgramContent}. |
| */ |
| private final class UpdatePreviewProgramTask extends AsyncTask<Void, Void, Void> { |
| private long mPreviewChannelId; |
| private Set<PreviewProgramContent> mPrograms; |
| private Map<Long, Long> mCurrentProgramId2PreviewProgramId; |
| private Set<PreviewDataListener> mPreviewDataListeners = new CopyOnWriteArraySet<>(); |
| |
| public UpdatePreviewProgramTask( |
| long previewChannelId, Set<PreviewProgramContent> programs) { |
| mPreviewChannelId = previewChannelId; |
| mPrograms = programs; |
| if (mPreviewData.getPreviewProgramIds(previewChannelId) == null) { |
| mCurrentProgramId2PreviewProgramId = new HashMap<>(); |
| } else { |
| mCurrentProgramId2PreviewProgramId = |
| new HashMap<>(mPreviewData.getPreviewProgramIds(previewChannelId)); |
| } |
| } |
| |
| public void addPreviewDataListener(PreviewDataListener previewDataListener) { |
| if (previewDataListener != null) { |
| mPreviewDataListeners.add(previewDataListener); |
| } |
| } |
| |
| public void addPreviewDataListeners(Set<PreviewDataListener> previewDataListeners) { |
| if (previewDataListeners != null) { |
| mPreviewDataListeners.addAll(previewDataListeners); |
| } |
| } |
| |
| public Set<PreviewProgramContent> getPrograms() { |
| return mPrograms; |
| } |
| |
| public Set<PreviewDataListener> getPreviewDataListeners() { |
| return mPreviewDataListeners; |
| } |
| |
| @Override |
| protected Void doInBackground(Void... params) { |
| if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground"); |
| Map<Long, Long> uncheckedPrograms = new HashMap<>(mCurrentProgramId2PreviewProgramId); |
| for (PreviewProgramContent program : mPrograms) { |
| if (isCancelled()) { |
| return null; |
| } |
| Long existingPreviewProgramId = uncheckedPrograms.remove(program.getId()); |
| if (existingPreviewProgramId != null) { |
| if (DEBUG) |
| Log.d( |
| TAG, |
| "Preview program " |
| + existingPreviewProgramId |
| + " " |
| + "already exists for program " |
| + program.getId()); |
| continue; |
| } |
| try { |
| Uri programUri = |
| mContentResolver.insert( |
| TvContract.PreviewPrograms.CONTENT_URI, |
| PreviewDataUtils.createPreviewProgramFromContent(program) |
| .toContentValues()); |
| if (programUri != null) { |
| long previewProgramId = ContentUris.parseId(programUri); |
| mCurrentProgramId2PreviewProgramId.put(program.getId(), previewProgramId); |
| if (DEBUG) Log.d(TAG, "Add new preview program " + previewProgramId); |
| } else { |
| Log.e(TAG, "Fail to insert preview program"); |
| } |
| } catch (Exception e) { |
| Log.e(TAG, "Fail to get preview program ID"); |
| } |
| } |
| |
| for (Long key : uncheckedPrograms.keySet()) { |
| if (isCancelled()) { |
| return null; |
| } |
| try { |
| if (DEBUG) Log.d(TAG, "Remove preview program " + uncheckedPrograms.get(key)); |
| mContentResolver.delete( |
| TvContract.buildPreviewProgramUri(uncheckedPrograms.get(key)), |
| null, |
| null); |
| mCurrentProgramId2PreviewProgramId.remove(key); |
| } catch (Exception e) { |
| Log.e(TAG, "Fail to remove preview program " + uncheckedPrograms.get(key)); |
| } |
| } |
| return null; |
| } |
| |
| @Override |
| protected void onPostExecute(Void result) { |
| super.onPostExecute(result); |
| mPreviewData.setPreviewProgramIds( |
| mPreviewChannelId, mCurrentProgramId2PreviewProgramId); |
| mUpdatePreviewProgramTasks.remove(mPreviewChannelId); |
| for (PreviewDataListener previewDataListener : mPreviewDataListeners) { |
| previewDataListener.onPreviewDataUpdateFinished(); |
| } |
| } |
| |
| public void saveStatus() { |
| mPreviewData.setPreviewProgramIds( |
| mPreviewChannelId, mCurrentProgramId2PreviewProgramId); |
| } |
| } |
| |
| /** Class to store the query result of preview data. */ |
| private static final class PreviewData { |
| private Map<Long, Long> mPreviewChannelType2Id = new HashMap<>(); |
| private Map<Long, Map<Long, Long>> mProgramId2PreviewProgramId = new HashMap<>(); |
| |
| PreviewData() { |
| mPreviewChannelType2Id = new HashMap<>(); |
| mProgramId2PreviewProgramId = new HashMap<>(); |
| } |
| |
| PreviewData(PreviewData previewData) { |
| mPreviewChannelType2Id = new HashMap<>(previewData.mPreviewChannelType2Id); |
| mProgramId2PreviewProgramId = new HashMap<>(previewData.mProgramId2PreviewProgramId); |
| } |
| |
| public void addPreviewProgram(PreviewProgram previewProgram) { |
| long previewChannelId = previewProgram.getChannelId(); |
| Map<Long, Long> programId2PreviewProgram = |
| mProgramId2PreviewProgramId.get(previewChannelId); |
| if (programId2PreviewProgram == null) { |
| programId2PreviewProgram = new HashMap<>(); |
| } |
| mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgram); |
| if (previewProgram.getInternalProviderId() != null) { |
| programId2PreviewProgram.put( |
| Long.parseLong(previewProgram.getInternalProviderId()), |
| previewProgram.getId()); |
| } |
| } |
| |
| public @PreviewChannelType long getPreviewChannelId(long previewChannelType) { |
| Long result = mPreviewChannelType2Id.get(previewChannelType); |
| return result == null ? INVALID_PREVIEW_CHANNEL_ID : result; |
| } |
| |
| public Map<Long, Long> getAllPreviewChannelIds() { |
| return mPreviewChannelType2Id; |
| } |
| |
| public void addPreviewChannelId(long previewChannelType, long previewChannelId) { |
| mPreviewChannelType2Id.put(previewChannelType, previewChannelId); |
| } |
| |
| public void removePreviewChannelId(long previewChannelType) { |
| mPreviewChannelType2Id.remove(previewChannelType); |
| } |
| |
| public void removePreviewChannel(long previewChannelId) { |
| removePreviewChannelId(previewChannelId); |
| removePreviewProgramIds(previewChannelId); |
| } |
| |
| public Map<Long, Long> getPreviewProgramIds(long previewChannelId) { |
| return mProgramId2PreviewProgramId.get(previewChannelId); |
| } |
| |
| public Map<Long, Map<Long, Long>> getAllPreviewProgramIds() { |
| return mProgramId2PreviewProgramId; |
| } |
| |
| public void setPreviewProgramIds( |
| long previewChannelId, Map<Long, Long> programId2PreviewProgramId) { |
| mProgramId2PreviewProgramId.put(previewChannelId, programId2PreviewProgramId); |
| } |
| |
| public void removePreviewProgramIds(long previewChannelId) { |
| mProgramId2PreviewProgramId.remove(previewChannelId); |
| } |
| } |
| |
| /** A utils class for preview data. */ |
| public static final class PreviewDataUtils { |
| /** Creates a preview channel. */ |
| public static android.support.media.tv.Channel createPreviewChannel( |
| Context context, @PreviewChannelType long previewChannelType) { |
| if (previewChannelType == TYPE_RECORDED_PROGRAM_PREVIEW_CHANNEL) { |
| return createRecordedProgramPreviewChannel(context, previewChannelType); |
| } |
| return createDefaultPreviewChannel(context, previewChannelType); |
| } |
| |
| private static android.support.media.tv.Channel createDefaultPreviewChannel( |
| Context context, @PreviewChannelType long previewChannelType) { |
| android.support.media.tv.Channel.Builder builder = |
| new android.support.media.tv.Channel.Builder(); |
| CharSequence appLabel = |
| context.getApplicationInfo().loadLabel(context.getPackageManager()); |
| CharSequence appDescription = |
| context.getApplicationInfo().loadDescription(context.getPackageManager()); |
| builder.setType(TvContract.Channels.TYPE_PREVIEW) |
| .setDisplayName(appLabel == null ? null : appLabel.toString()) |
| .setDescription(appDescription == null ? null : appDescription.toString()) |
| .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI) |
| .setInternalProviderFlag1(previewChannelType); |
| return builder.build(); |
| } |
| |
| private static android.support.media.tv.Channel createRecordedProgramPreviewChannel( |
| Context context, @PreviewChannelType long previewChannelType) { |
| android.support.media.tv.Channel.Builder builder = |
| new android.support.media.tv.Channel.Builder(); |
| builder.setType(TvContract.Channels.TYPE_PREVIEW) |
| .setDisplayName( |
| context.getResources() |
| .getString(R.string.recorded_programs_preview_channel)) |
| .setAppLinkIntentUri(TvContract.Channels.CONTENT_URI) |
| .setInternalProviderFlag1(previewChannelType); |
| return builder.build(); |
| } |
| |
| /** Creates a preview program. */ |
| public static PreviewProgram createPreviewProgramFromContent( |
| PreviewProgramContent program) { |
| PreviewProgram.Builder builder = new PreviewProgram.Builder(); |
| builder.setChannelId(program.getPreviewChannelId()) |
| .setType(program.getType()) |
| .setLive(program.getLive()) |
| .setTitle(program.getTitle()) |
| .setDescription(program.getDescription()) |
| .setPosterArtUri(program.getPosterArtUri()) |
| .setIntentUri(program.getIntentUri()) |
| .setPreviewVideoUri(program.getPreviewVideoUri()) |
| .setInternalProviderId(Long.toString(program.getId())) |
| .setContentId(program.getIntentUri().toString()); |
| return builder.build(); |
| } |
| |
| /** Appends query parameters to a Uri. */ |
| public static Uri addQueryParamToUri(Uri uri, Pair<String, String> param) { |
| return uri.buildUpon().appendQueryParameter(param.first, param.second).build(); |
| } |
| } |
| } |