/* * 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.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 mPreviewDataListeners = new CopyOnWriteArraySet<>(); private QueryPreviewDataTask mQueryPreviewTask; private final Map mCreatePreviewChannelTasks = new HashMap<>(); private final Map 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 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 { 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 (Exception 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 { private final long mPreviewChannelType; private Set 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 { private long mPreviewChannelId; private Set mPrograms; private Map mCurrentProgramId2PreviewProgramId; private Set mPreviewDataListeners = new CopyOnWriteArraySet<>(); public UpdatePreviewProgramTask( long previewChannelId, Set 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 previewDataListeners) { if (previewDataListeners != null) { mPreviewDataListeners.addAll(previewDataListeners); } } public Set getPrograms() { return mPrograms; } public Set getPreviewDataListeners() { return mPreviewDataListeners; } @Override protected Void doInBackground(Void... params) { if (DEBUG) Log.d(TAG, "UpdatePreviewProgamTask.doInBackground"); Map 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 mPreviewChannelType2Id = new HashMap<>(); private Map> 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 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 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 getPreviewProgramIds(long previewChannelId) { return mProgramId2PreviewProgramId.get(previewChannelId); } public Map> getAllPreviewProgramIds() { return mProgramId2PreviewProgramId; } public void setPreviewProgramIds( long previewChannelId, Map 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 param) { return uri.buildUpon().appendQueryParameter(param.first, param.second).build(); } } }