package org.edx.mobile.module.storage; import android.app.DownloadManager; import android.content.Context; import android.media.MediaMetadataRetriever; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.inject.Inject; import com.google.inject.Singleton; import org.edx.mobile.interfaces.SectionItemInterface; import org.edx.mobile.logger.Logger; import org.edx.mobile.model.VideoModel; import org.edx.mobile.model.api.ChapterModel; import org.edx.mobile.model.api.EnrolledCoursesResponse; import org.edx.mobile.model.api.ProfileModel; import org.edx.mobile.model.api.SectionEntry; import org.edx.mobile.model.api.SectionItemModel; import org.edx.mobile.model.api.VideoResponseModel; import org.edx.mobile.model.course.VideoBlockModel; import org.edx.mobile.model.db.DownloadEntry; import org.edx.mobile.model.download.NativeDownloadModel; import org.edx.mobile.module.db.DataCallback; import org.edx.mobile.module.db.DatabaseModelFactory; import org.edx.mobile.module.db.IDatabase; import org.edx.mobile.module.db.impl.DatabaseFactory; import org.edx.mobile.module.download.IDownloadManager; import org.edx.mobile.module.prefs.LoginPrefs; import org.edx.mobile.module.prefs.UserPrefs; import org.edx.mobile.services.ServiceManager; import org.edx.mobile.user.UserAPI; import org.edx.mobile.util.Config; import org.edx.mobile.util.NetworkUtil; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import de.greenrobot.event.EventBus; @Singleton public class Storage implements IStorage { @Inject private Context context; @Inject private IDatabase db; @Inject private IDownloadManager dm; @Inject private UserPrefs pref; @Inject private Config config; @Inject private LoginPrefs loginPrefs; @Inject ServiceManager serviceManager; @Inject UserAPI api; private final Logger logger = new Logger(getClass().getName()); public long addDownload(VideoModel model) { if(model.getVideoUrl()==null||model.getVideoUrl().length()<=0){ return -1; } VideoModel videoByUrl = db.getVideoByVideoUrl(model.getVideoUrl(), null); db.addVideoData(model, null); if(model.isVideoForWebOnly()) return -1; //we may need to return different error code. //but for now we show same generic error message //IVideoModel videoById = db.getVideoEntryByVideoId(model.getVideoId(), null); if (videoByUrl == null || videoByUrl.getDmId() < 0) { boolean downloadPreference = pref.isDownloadOverWifiOnly(); if(NetworkUtil.isOnZeroRatedNetwork(context, config)){ //If the device has zero rated network, then allow downloading //on mobile network even if user has "Only on wifi" settings as ON downloadPreference = false; } // Fail the download if download directory isn't available final File downloadDirectory = pref.getDownloadDirectory(); if (downloadDirectory == null) return -1; // there is no any download ever marked for this URL // so, add a download and map download info to given video long dmid = dm.addDownload(downloadDirectory, model.getVideoUrl(), downloadPreference); if(dmid==-1){ //Download did not start for the video because of an issue in DownloadManager return -1; } NativeDownloadModel download = dm.getDownload(dmid); if(download!=null){ // copy download info model.setDownloadingInfo(download); } } else { // download for this URL already exists, just map download info to given video model.setDownloadInfo(videoByUrl); } db.updateDownloadingVideoInfoByVideoId(model, new DataCallback<Integer>() { @Override public void onResult(Integer noOfRows) { if (noOfRows > 1) { logger.warn("Should have updated only one video, " + "but seems more than one videos are updated"); } logger.debug("Video download info updated for " + noOfRows + " videos"); } @Override public void onFail(Exception ex) { logger.error(ex); } }); return model.getDmId(); } public int removeDownload(VideoModel model) { int count = db.getVideoCountByVideoUrl(model.getVideoUrl(), null); if (count <= 1) { // if only one video exists, then mark it as DELETED // Also, remove its downloaded file dm.removeDownload(model.getDmId()); deleteFile(model.getFilePath()); } // anyways, we mark the video as DELETED int videosDeleted = db.deleteVideoByVideoId(model, null); EventBus.getDefault().post(new DownloadedVideoDeletedEvent()); return videosDeleted; } /** * Deletes the physical file identified by given absolute file path. * Returns true if delete succeeds or if file does NOT exist, false otherwise. * DownloadManager actually deletes the physical file when remove method is called. * So, this method might not be required for removing downloads. * @param filepath The file to delete * @return true if delete succeeds or if file does NOT exist, false otherwise. */ private boolean deleteFile(String filepath) { try { if(filepath != null) { File file = new File(filepath); if (file.exists()) { if (file.delete()) { logger.debug("Deleted: " + file.getPath()); return true; } else { logger.warn("Delete failed: " + file.getPath()); } } else { logger.warn("Delete failed, file does NOT exist: " + file.getPath()); return true; } } } catch(Exception e) { logger.error(e); } return false; } @Override public int deleteAllUnenrolledVideos() { // Integer count = db.deletedDeactivatedVideos(); return 0; } @Override public void getAverageDownloadProgressInChapter(String enrollmentId, String chapter, final DataCallback<Integer> callback) { List<Long> dmidList = db.getDownloadingVideoDmIdsForChapter(enrollmentId, chapter, null); if (dmidList == null || dmidList.isEmpty()) { callback.onResult(0); return; } try { long[] dmidArray = new long[dmidList.size()]; for (int i=0; i< dmidList.size(); i++) { dmidArray[i] = dmidList.get(i); } int progress = dm.getAverageProgressForDownloads(dmidArray); callback.sendResult(progress); } catch(Exception ex) { callback.sendException(ex); logger.error(ex); } } @Override public void getAverageDownloadProgress(final DataCallback<Integer> callback) { IDatabase db = DatabaseFactory.getInstance( DatabaseFactory.TYPE_DATABASE_NATIVE ); db.getListOfOngoingDownloads(new DataCallback<List<VideoModel>>() { @Override public void onResult(List<VideoModel> result) { long[] dmids = new long[result.size()]; for (int i=0; i< result.size(); i++) { dmids[i] = result.get(i).getDmId(); logger.debug("xxxxxxxx =" +dmids[i]); } int averageProgress = dm.getAverageProgressForDownloads(dmids); callback.onResult(averageProgress); } @Override public void onFail(Exception ex) { callback.onFail(ex); } }); } @Override public void getAverageDownloadProgressInSection(String enrollmentId, String chapter, String section, DataCallback<Integer> callback) { long[] dmidArray = db.getDownloadingVideoDmIdsForSection(enrollmentId, chapter, section, null); if (dmidArray == null || dmidArray.length == 0) { callback.onResult(0); return; } try { int progress = dm.getAverageProgressForDownloads(dmidArray); callback.sendResult(progress); } catch(Exception ex) { logger.error(ex); callback.sendException(ex); } } @Override public VideoModel getDownloadEntryfromVideoResponseModel( VideoResponseModel vrm) { VideoModel video = db.getVideoEntryByVideoId(vrm.getSummary().getId(), null); if (video != null) { // we have a db entry, so return it return video; } return DatabaseModelFactory.getModel(vrm); } @Override public VideoModel getDownloadEntryFromVideoModel(VideoBlockModel block){ VideoModel video = db.getVideoEntryByVideoId(block.getId(), null); if (video != null) { return video; } return DatabaseModelFactory.getModel(block.getData(), block); } @Override public NativeDownloadModel getNativeDownload(long dmId) { return dm.getDownload(dmId); } @Override @NonNull public ArrayList<EnrolledCoursesResponse> getDownloadedCoursesWithVideoCountAndSize() throws Exception { ArrayList<EnrolledCoursesResponse> downloadedCourseList = new ArrayList<>(); String username = getUsername(); String org = config.getOrganizationCode(); if (username != null) { for(EnrolledCoursesResponse enrolledCoursesResponse : api.getUserEnrolledCourses(username, org, true)){ int videoCount = db.getDownloadedVideoCountByCourse( enrolledCoursesResponse.getCourse().getId(),null); if(videoCount>0){ enrolledCoursesResponse.videoCount = videoCount; enrolledCoursesResponse.size = db.getDownloadedVideosSizeByCourse( enrolledCoursesResponse.getCourse().getId(),null); downloadedCourseList.add(enrolledCoursesResponse); } } } return downloadedCourseList; } @Nullable private String getUsername() { String ret = null; ProfileModel profile = pref.getProfile(); if (profile != null) { ret = profile.username; } return ret; } @Override @NonNull public ArrayList<SectionItemInterface> getRecentDownloadedVideosList() throws Exception { ArrayList<SectionItemInterface> recentVideolist = new ArrayList<>(); String username = getUsername(); String org = config.getOrganizationCode(); if (username != null) { for (final EnrolledCoursesResponse course : api.getUserEnrolledCourses(username, org, true)) { // add all videos to the list for this course List<VideoModel> videos = db.getSortedDownloadsByDownloadedDateForCourseId( course.getCourse().getId(), null); // ArrayList<IVideoModel> videos = new ArrayList<IVideoModel>(); if (videos != null && videos.size() > 0) { // add course header to the list recentVideolist.add(course); for (VideoModel videoModel : videos) { //TODO : Need to check how SectionItemInterface can be converted to IVideoModel recentVideolist.add((SectionItemInterface) videoModel); } } } } return recentVideolist; } @Override public DownloadEntry reloadDownloadEntry(DownloadEntry video) { try{ DownloadEntry de = (DownloadEntry) db.getVideoEntryByVideoId(video.videoId, null); if (de != null) { video.lastPlayedOffset = de.lastPlayedOffset; video.watched = de.watched; video.downloaded = de.downloaded; } return video; } catch(Exception ex) { logger.error(ex); } return null; } @Override public void getDownloadProgressByDmid(long dmId, DataCallback<Integer> callback) { if (dmId == 0) { callback.onResult(0); return; } try { long[] dmidArray = new long[1]; dmidArray[0] = dmId; int progress = dm.getAverageProgressForDownloads(dmidArray); callback.sendResult(progress); } catch(Exception ex) { logger.error(ex); callback.sendException(ex); } } @Override public ArrayList<SectionItemInterface> getSortedOrganizedVideosByCourse( String courseId) { ArrayList<SectionItemInterface> list = new ArrayList<>(); ArrayList<VideoModel> downloadList = (ArrayList<VideoModel>) db .getDownloadedVideoListForCourse(courseId, null); if(downloadList==null||downloadList.size()==0){ return list; } try { Map<String, SectionEntry> courseHeirarchyMap = serviceManager.getCourseHierarchy(courseId); // iterate chapters for (Entry<String, SectionEntry> chapterentry : courseHeirarchyMap.entrySet()) { boolean chapterAddedFlag=false; // iterate lectures for (Entry<String, ArrayList<VideoResponseModel>> lectureEntry : chapterentry.getValue().sections.entrySet()) { boolean lectureAddedFlag=false; // iterate videos for (VideoResponseModel v : lectureEntry.getValue()) { for(VideoModel de : downloadList){ // identify the video if (de.getVideoId().equalsIgnoreCase(v.getSummary().getId())) { // add this chapter to the list if(!chapterAddedFlag){ ChapterModel chModel = new ChapterModel(); chModel.name = chapterentry.getKey(); list.add(chModel); chapterAddedFlag = true; } if(!lectureAddedFlag){ SectionItemModel lectureModel = new SectionItemModel(); lectureModel.name = lectureEntry.getKey(); list.add(lectureModel); lectureAddedFlag = true; } // add section below this chapter list.add((DownloadEntry)de); break; } // If condition for videoId } //for loop for downloadedvideos for CourseId } // for loop for VRM } // For loop for lectures } // For loop for Chapters return list; } catch (Exception e) { logger.error(e); } return null; } @Override public void markDownloadAsComplete(long dmId, DataCallback<VideoModel> callback) { try{ NativeDownloadModel nm = dm.getDownload(dmId); if (nm != null && nm.status == DownloadManager.STATUS_SUCCESSFUL) { { DownloadEntry e = (DownloadEntry) db.getDownloadEntryByDmId(dmId, null); e.downloaded = DownloadEntry.DownloadedState.DOWNLOADED; e.filepath = nm.filepath; if(e.size<=0){ e.size = nm.size; } e.downloadedOn = System.currentTimeMillis(); // update file duration if(e.duration==0){ try { MediaMetadataRetriever r = new MediaMetadataRetriever(); FileInputStream in = new FileInputStream(new File(e.filepath)); r.setDataSource(in.getFD()); int duration = Integer .parseInt(r .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); e.duration = duration/1000; logger.debug("Duration updated to : " + duration); in.close(); } catch (Exception ex) { logger.error(ex); } } db.updateDownloadCompleteInfoByDmId(dmId, e, null); callback.sendResult(e); EventBus.getDefault().post(new DownloadCompletedEvent()); } } else { // download not yet successful logger.debug("Download not yet completed"); } }catch(Exception e){ callback.sendException(e); logger.error(e); } } /** * Checks progress of all the videos that are being downloaded. * If progress of any of the downloads is 100%, then marks the video as DOWNLOADED. * NOTE - precondition - used only for app upgrade */ public void repairDownloadCompletionData() { // attempt to repair the data Thread maintenanceThread = new Thread(new Runnable() { @Override public void run() { try { ProfileModel profile = loginPrefs.getCurrentUserProfile(); if (profile == null) { // user no logged in return; } List<Long> dmidList = db.getAllDownloadingVideosDmidList(null); for (Long d : dmidList) { // for each downloading video, check the percentage progress boolean downloadComplete = dm.isDownloadComplete(d); if (downloadComplete) { // this means download is completed // so the video status should be marked as DOWNLOADED, not DOWNLOADING // update the video status markDownloadAsComplete(d, new DataCallback<VideoModel>() { @Override public void onResult(VideoModel result) { logger.debug("Video download marked as completed, dmid=" + result.getDmId()); } @Override public void onFail(Exception ex) { logger.error(ex); } }); } } } catch (Exception ex) { logger.error(ex); } } }); maintenanceThread.start(); } @Override public void markVideoPlaying(DownloadEntry videoModel, final DataCallback<Integer> watchedStateCallback) { try { final DownloadEntry v = videoModel; if (v != null) { if (v.watched == DownloadEntry.WatchedState.UNWATCHED) { videoModel.watched = DownloadEntry.WatchedState.PARTIALLY_WATCHED; // video entry might not exist in the database, add it db.addVideoData(videoModel, new DataCallback<Long>() { @Override public void onResult(Long result) { try { // mark this as partially watches, as playing has started db.updateVideoWatchedState(v.getVideoId(), DownloadEntry.WatchedState.PARTIALLY_WATCHED, watchedStateCallback); } catch (Exception ex) { logger.error(ex); } } @Override public void onFail(Exception ex) { logger.error(ex); } }); } } } catch (Exception ex) { logger.error(ex); } } }