Home | History | Annotate | Download | only in data
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.data;
     18 
     19 import android.content.ContentProviderOperation;
     20 import android.content.Context;
     21 import android.content.OperationApplicationException;
     22 import android.content.SharedPreferences;
     23 import android.graphics.Bitmap.CompressFormat;
     24 import android.media.tv.TvContract;
     25 import android.net.Uri;
     26 import android.os.AsyncTask;
     27 import android.os.RemoteException;
     28 import android.support.annotation.MainThread;
     29 import android.text.TextUtils;
     30 import android.util.Log;
     31 import com.android.tv.common.util.PermissionUtils;
     32 import com.android.tv.common.util.SharedPreferencesUtils;
     33 import com.android.tv.data.api.Channel;
     34 import com.android.tv.util.images.BitmapUtils;
     35 import com.android.tv.util.images.BitmapUtils.ScaledBitmapInfo;
     36 import java.io.IOException;
     37 import java.io.OutputStream;
     38 import java.util.ArrayList;
     39 import java.util.List;
     40 import java.util.Map;
     41 
     42 /**
     43  * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
     44  * or need update logos. This class is thread safe.
     45  */
     46 public class ChannelLogoFetcher {
     47     private static final String TAG = "ChannelLogoFetcher";
     48     private static final boolean DEBUG = false;
     49 
     50     private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
     51             "is_first_time_fetch_channel_logo";
     52 
     53     private static FetchLogoTask sFetchTask;
     54 
     55     /**
     56      * Fetches the channel logos from the cloud data and insert them into TvProvider. The previous
     57      * task is canceled and a new task starts.
     58      */
     59     @MainThread
     60     public static void startFetchingChannelLogos(Context context, List<Channel> channels) {
     61         if (!PermissionUtils.hasAccessAllEpg(context)) {
     62             // TODO: support this feature for non-system LC app. b/23939816
     63             return;
     64         }
     65         if (sFetchTask != null) {
     66             sFetchTask.cancel(true);
     67             sFetchTask = null;
     68         }
     69         if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
     70         if (channels == null || channels.isEmpty()) {
     71             return;
     72         }
     73         sFetchTask = new FetchLogoTask(context.getApplicationContext(), channels);
     74         sFetchTask.execute();
     75     }
     76 
     77     private ChannelLogoFetcher() {}
     78 
     79     private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
     80         private final Context mContext;
     81         private final List<Channel> mChannels;
     82 
     83         private FetchLogoTask(Context context, List<Channel> channels) {
     84             mContext = context;
     85             mChannels = channels;
     86         }
     87 
     88         @Override
     89         protected Void doInBackground(Void... arg) {
     90             if (isCancelled()) {
     91                 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
     92                 return null;
     93             }
     94             List<Channel> channelsToUpdate = new ArrayList<>();
     95             List<Channel> channelsToRemove = new ArrayList<>();
     96             // Updates or removes the logo by comparing the logo uri which is got from the cloud
     97             // and the stored one. And we assume that the data got form the cloud is 100%
     98             // correct and completed.
     99             SharedPreferences sharedPreferences =
    100                     mContext.getSharedPreferences(
    101                             SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
    102                             Context.MODE_PRIVATE);
    103             SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
    104             Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
    105             boolean isFirstTimeFetchChannelLogo =
    106                     sharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
    107             // Iterating channels.
    108             for (Channel channel : mChannels) {
    109                 String channelIdString = Long.toString(channel.getId());
    110                 String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
    111                 if (!TextUtils.isEmpty(channel.getLogoUri())
    112                         && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
    113                     channelsToUpdate.add(channel);
    114                     sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
    115                 } else if (TextUtils.isEmpty(channel.getLogoUri())
    116                         && (!TextUtils.isEmpty(storedChannelLogoUri)
    117                                 || isFirstTimeFetchChannelLogo)) {
    118                     channelsToRemove.add(channel);
    119                     sharedPreferencesEditor.remove(channelIdString);
    120                 }
    121             }
    122 
    123             // Removes non existing channels from SharedPreferences.
    124             for (String channelId : uncheckedChannels.keySet()) {
    125                 sharedPreferencesEditor.remove(channelId);
    126             }
    127 
    128             // Updates channel logos.
    129             for (Channel channel : channelsToUpdate) {
    130                 if (isCancelled()) {
    131                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    132                     return null;
    133                 }
    134                 // Downloads the channel logo.
    135                 String logoUri = channel.getLogoUri();
    136                 ScaledBitmapInfo bitmapInfo =
    137                         BitmapUtils.decodeSampledBitmapFromUriString(
    138                                 mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
    139                 if (bitmapInfo == null) {
    140                     Log.e(
    141                             TAG,
    142                             "Failed to load bitmap. {channelName="
    143                                     + channel.getDisplayName()
    144                                     + ", "
    145                                     + "logoUri="
    146                                     + logoUri
    147                                     + "}");
    148                     continue;
    149                 }
    150                 if (isCancelled()) {
    151                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    152                     return null;
    153                 }
    154 
    155                 // Inserts the logo to DB.
    156                 Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
    157                 try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
    158                     bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
    159                 } catch (IOException e) {
    160                     Log.e(TAG, "Failed to write " + logoUri + "  to " + dstLogoUri, e);
    161                     // Removes it from the shared preference for the failed channels to make it
    162                     // retry next time.
    163                     sharedPreferencesEditor.remove(Long.toString(channel.getId()));
    164                     continue;
    165                 }
    166                 if (DEBUG) {
    167                     Log.d(
    168                             TAG,
    169                             "Inserting logo file to DB succeeded. {from="
    170                                     + logoUri
    171                                     + ", to="
    172                                     + dstLogoUri
    173                                     + "}");
    174                 }
    175             }
    176 
    177             // Removes the logos for the channels that have logos before but now
    178             // their logo uris are null.
    179             boolean deleteChannelLogoFailed = false;
    180             if (!channelsToRemove.isEmpty()) {
    181                 ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    182                 for (Channel channel : channelsToRemove) {
    183                     ops.add(
    184                             ContentProviderOperation.newDelete(
    185                                             TvContract.buildChannelLogoUri(channel.getId()))
    186                                     .build());
    187                 }
    188                 try {
    189                     mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
    190                 } catch (RemoteException | OperationApplicationException e) {
    191                     deleteChannelLogoFailed = true;
    192                     Log.e(TAG, "Error deleting obsolete channels", e);
    193                 }
    194             }
    195             if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) {
    196                 sharedPreferencesEditor.putBoolean(
    197                         PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false);
    198             }
    199             sharedPreferencesEditor.commit();
    200             if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
    201             return null;
    202         }
    203 
    204         @Override
    205         protected void onPostExecute(Void result) {
    206             sFetchTask = null;
    207         }
    208     }
    209 }
    210