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 
     32 import com.android.tv.common.SharedPreferencesUtils;
     33 import com.android.tv.util.BitmapUtils;
     34 import com.android.tv.util.BitmapUtils.ScaledBitmapInfo;
     35 import com.android.tv.util.PermissionUtils;
     36 
     37 import java.io.IOException;
     38 import java.io.OutputStream;
     39 import java.util.ArrayList;
     40 import java.util.Map;
     41 import java.util.List;
     42 
     43 /**
     44  * Fetches channel logos from the cloud into the database. It's for the channels which have no logos
     45  * or need update logos. This class is thread safe.
     46  */
     47 public class ChannelLogoFetcher {
     48     private static final String TAG = "ChannelLogoFetcher";
     49     private static final boolean DEBUG = false;
     50 
     51     private static final String PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO =
     52             "is_first_time_fetch_channel_logo";
     53 
     54     private static FetchLogoTask sFetchTask;
     55 
     56     /**
     57      * Fetches the channel logos from the cloud data and insert them into TvProvider.
     58      * The previous task is canceled and a new task starts.
     59      */
     60     @MainThread
     61     public static void startFetchingChannelLogos(
     62             Context context, List<Channel> channels) {
     63         if (!PermissionUtils.hasAccessAllEpg(context)) {
     64             // TODO: support this feature for non-system LC app. b/23939816
     65             return;
     66         }
     67         if (sFetchTask != null) {
     68             sFetchTask.cancel(true);
     69             sFetchTask = null;
     70         }
     71         if (DEBUG) Log.d(TAG, "Request to start fetching logos.");
     72         if (channels == null || channels.isEmpty()) {
     73             return;
     74         }
     75         sFetchTask = new FetchLogoTask(context.getApplicationContext(), channels);
     76         sFetchTask.execute();
     77     }
     78 
     79     private ChannelLogoFetcher() {
     80     }
     81 
     82     private static final class FetchLogoTask extends AsyncTask<Void, Void, Void> {
     83         private final Context mContext;
     84         private final List<Channel> mChannels;
     85 
     86         private FetchLogoTask(Context context, List<Channel> channels) {
     87             mContext = context;
     88             mChannels = channels;
     89         }
     90 
     91         @Override
     92         protected Void doInBackground(Void... arg) {
     93             if (isCancelled()) {
     94                 if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
     95                 return null;
     96             }
     97             List<Channel> channelsToUpdate = new ArrayList<>();
     98             List<Channel> channelsToRemove = new ArrayList<>();
     99             // Updates or removes the logo by comparing the logo uri which is got from the cloud
    100             // and the stored one. And we assume that the data got form the cloud is 100%
    101             // correct and completed.
    102             SharedPreferences sharedPreferences =
    103                     mContext.getSharedPreferences(
    104                             SharedPreferencesUtils.SHARED_PREF_CHANNEL_LOGO_URIS,
    105                             Context.MODE_PRIVATE);
    106             SharedPreferences.Editor sharedPreferencesEditor = sharedPreferences.edit();
    107             Map<String, ?> uncheckedChannels = sharedPreferences.getAll();
    108             boolean isFirstTimeFetchChannelLogo = sharedPreferences.getBoolean(
    109                     PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, true);
    110             // Iterating channels.
    111             for (Channel channel : mChannels) {
    112                 String channelIdString = Long.toString(channel.getId());
    113                 String storedChannelLogoUri = (String) uncheckedChannels.remove(channelIdString);
    114                 if (!TextUtils.isEmpty(channel.getLogoUri())
    115                         && !TextUtils.equals(storedChannelLogoUri, channel.getLogoUri())) {
    116                     channelsToUpdate.add(channel);
    117                     sharedPreferencesEditor.putString(channelIdString, channel.getLogoUri());
    118                 } else if (TextUtils.isEmpty(channel.getLogoUri())
    119                         && (!TextUtils.isEmpty(storedChannelLogoUri)
    120                         || isFirstTimeFetchChannelLogo)) {
    121                     channelsToRemove.add(channel);
    122                     sharedPreferencesEditor.remove(channelIdString);
    123                 }
    124             }
    125 
    126             // Removes non existing channels from SharedPreferences.
    127             for (String channelId : uncheckedChannels.keySet()) {
    128                 sharedPreferencesEditor.remove(channelId);
    129             }
    130 
    131             // Updates channel logos.
    132             for (Channel channel : channelsToUpdate) {
    133                 if (isCancelled()) {
    134                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    135                     return null;
    136                 }
    137                 // Downloads the channel logo.
    138                 String logoUri = channel.getLogoUri();
    139                 ScaledBitmapInfo bitmapInfo = BitmapUtils.decodeSampledBitmapFromUriString(
    140                         mContext, logoUri, Integer.MAX_VALUE, Integer.MAX_VALUE);
    141                 if (bitmapInfo == null) {
    142                     Log.e(TAG, "Failed to load bitmap. {channelName=" + channel.getDisplayName()
    143                             + ", " + "logoUri=" + logoUri + "}");
    144                     continue;
    145                 }
    146                 if (isCancelled()) {
    147                     if (DEBUG) Log.d(TAG, "Fetching the channel logos has been canceled");
    148                     return null;
    149                 }
    150 
    151                 // Inserts the logo to DB.
    152                 Uri dstLogoUri = TvContract.buildChannelLogoUri(channel.getId());
    153                 try (OutputStream os = mContext.getContentResolver().openOutputStream(dstLogoUri)) {
    154                     bitmapInfo.bitmap.compress(CompressFormat.PNG, 100, os);
    155                 } catch (IOException e) {
    156                     Log.e(TAG, "Failed to write " + logoUri + "  to " + dstLogoUri, e);
    157                     // Removes it from the shared preference for the failed channels to make it
    158                     // retry next time.
    159                     sharedPreferencesEditor.remove(Long.toString(channel.getId()));
    160                     continue;
    161                 }
    162                 if (DEBUG) {
    163                     Log.d(TAG, "Inserting logo file to DB succeeded. {from=" + logoUri + ", to="
    164                             + dstLogoUri + "}");
    165                 }
    166             }
    167 
    168             // Removes the logos for the channels that have logos before but now
    169             // their logo uris are null.
    170             boolean deleteChannelLogoFailed = false;
    171             if (!channelsToRemove.isEmpty()) {
    172                 ArrayList<ContentProviderOperation> ops = new ArrayList<>();
    173                 for (Channel channel : channelsToRemove) {
    174                     ops.add(ContentProviderOperation.newDelete(
    175                         TvContract.buildChannelLogoUri(channel.getId())).build());
    176                 }
    177                 try {
    178                     mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops);
    179                 } catch (RemoteException | OperationApplicationException e) {
    180                     deleteChannelLogoFailed = true;
    181                     Log.e(TAG, "Error deleting obsolete channels", e);
    182                 }
    183             }
    184             if (isFirstTimeFetchChannelLogo && !deleteChannelLogoFailed) {
    185                 sharedPreferencesEditor.putBoolean(
    186                         PREF_KEY_IS_FIRST_TIME_FETCH_CHANNEL_LOGO, false);
    187             }
    188             sharedPreferencesEditor.commit();
    189             if (DEBUG) Log.d(TAG, "Fetching logos has been finished successfully.");
    190             return null;
    191         }
    192 
    193         @Override
    194         protected void onPostExecute(Void result) {
    195             sFetchTask = null;
    196         }
    197     }
    198 }
    199