mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-25 12:27:38 +00:00 
			
		
		
		
	Improve DownloadManager and -Service
* Fix permission at some places * Fix access problem for downloaded files with external player * Store finished Downloads * Remove binding to DownloadService just to download a file * Javadoc * Code improvements
This commit is contained in:
		| @@ -46,5 +46,6 @@ dependencies { | ||||
|     compile 'com.google.code.gson:gson:2.4' | ||||
|     compile 'com.nononsenseapps:filepicker:3.0.0' | ||||
|     testCompile 'junit:junit:4.12' | ||||
|     testCompile 'org.mockito:mockito-core:1.10.19' | ||||
|     compile 'ch.acra:acra:4.9.0' | ||||
| } | ||||
|   | ||||
| @@ -161,6 +161,16 @@ | ||||
|         <activity | ||||
|             android:name=".ReCaptchaActivity" | ||||
|             android:label="@string/reCaptchaActivity" /> | ||||
|  | ||||
|         <provider | ||||
|             android:name="android.support.v4.content.FileProvider" | ||||
|             android:authorities="${applicationId}.provider" | ||||
|             android:exported="false" | ||||
|             android:grantUriPermissions="true"> | ||||
|             <meta-data | ||||
|                 android:name="android.support.FILE_PROVIDER_PATHS" | ||||
|                 android:resource="@xml/provider_paths"/> | ||||
|         </provider> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
| @@ -11,6 +11,7 @@ import android.view.MenuInflater; | ||||
| import android.view.MenuItem; | ||||
|  | ||||
| import org.schabi.newpipe.settings.SettingsActivity; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
|  | ||||
| /** | ||||
|  * Created by Christian Schabesberger on 02.08.16. | ||||
| @@ -74,6 +75,9 @@ public class MainActivity extends AppCompatActivity { | ||||
|                 return true; | ||||
|             } | ||||
|             case R.id.action_show_downloads: { | ||||
|                 if(!PermissionHelper.checkStoragePermissions(this)) { | ||||
|                     return false; | ||||
|                 } | ||||
|                 Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class); | ||||
|                 startActivity(intent); | ||||
|                 return true; | ||||
|   | ||||
| @@ -59,6 +59,7 @@ import org.schabi.newpipe.extractor.stream_info.VideoStream; | ||||
| import org.schabi.newpipe.player.BackgroundPlayer; | ||||
| import org.schabi.newpipe.player.PlayVideoActivity; | ||||
| import org.schabi.newpipe.player.ExoPlayerActivity; | ||||
| import org.schabi.newpipe.util.PermissionHelper; | ||||
|  | ||||
| import static android.app.Activity.RESULT_OK; | ||||
| import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; | ||||
| @@ -393,6 +394,10 @@ public class VideoItemDetailFragment extends Fragment { | ||||
|         actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { | ||||
|             @Override | ||||
|             public void onActionSelected(int selectedStreamId) { | ||||
|                 if(!PermissionHelper.checkStoragePermissions(getActivity())) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 try { | ||||
|                     Bundle args = new Bundle(); | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import android.support.v4.app.NavUtils; | ||||
| import android.support.v7.app.ActionBar; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.support.v7.widget.Toolbar; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuInflater; | ||||
| @@ -30,6 +31,7 @@ import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.report.ErrorActivity; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
| import org.schabi.newpipe.settings.SettingsActivity; | ||||
|  | ||||
| import java.io.File; | ||||
| @@ -53,26 +55,11 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O | ||||
|  | ||||
|  | ||||
|     private MissionsFragment mFragment; | ||||
|     private DownloadManager mManager; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
|  | ||||
|  | ||||
|     private String mPendingUrl; | ||||
|     private SharedPreferences mPrefs; | ||||
|  | ||||
|     private ServiceConnection mConnection = new ServiceConnection() { | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceConnected(ComponentName p1, IBinder binder) { | ||||
|             mBinder = (DownloadManagerService.DMBinder) binder; | ||||
|             mManager = mBinder.getDownloadManager(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceDisconnected(ComponentName p1) { | ||||
|  | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     @Override | ||||
|     @TargetApi(21) | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
| @@ -83,7 +70,6 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O | ||||
|         Intent i = new Intent(); | ||||
|         i.setClass(this, DownloadManagerService.class); | ||||
|         startService(i); | ||||
|         bindService(i, mConnection, Context.BIND_AUTO_CREATE); | ||||
|  | ||||
|         super.onCreate(savedInstanceState); | ||||
|         setContentView(R.layout.activity_downloader); | ||||
| @@ -91,7 +77,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O | ||||
|  | ||||
|         //noinspection ConstantConditions | ||||
|  | ||||
|         // its ok if this failes, we will catch that error later, and send it as report | ||||
|         // its ok if this fails, we will catch that error later, and send it as report | ||||
|         ActionBar actionBar = getSupportActionBar(); | ||||
|         actionBar.setDisplayHomeAsUpEnabled(true); | ||||
|         actionBar.setTitle(R.string.downloads_title); | ||||
| @@ -202,22 +188,24 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O | ||||
|             @Override | ||||
|             public boolean onMenuItemClick(MenuItem item) { | ||||
|                 if (item.getItemId() == R.id.okay) { | ||||
|  | ||||
|                     String location; | ||||
|                     if(audioButton.isChecked()) { | ||||
|                         location = NewPipeSettings.getAudioDownloadPath(DownloadActivity.this); | ||||
|                     } else { | ||||
|                         location = NewPipeSettings.getVideoDownloadPath(DownloadActivity.this); | ||||
|                     } | ||||
|  | ||||
|                     String fName = name.getText().toString().trim(); | ||||
|  | ||||
|                     File f = new File(mManager.getLocation() + "/" + fName); | ||||
|  | ||||
|                     File f = new File(location, fName); | ||||
|                     if (f.exists()) { | ||||
|                         Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show(); | ||||
|                     } else { | ||||
|  | ||||
|                         while (mBinder == null); | ||||
|  | ||||
|                         int res = mManager.startMission( | ||||
|                                 getIntent().getData().toString(), | ||||
|                                 fName, | ||||
|                                 audioButton.isChecked(), | ||||
|                                 threads.getProgress() + 1); | ||||
|                         mBinder.onMissionAdded(mManager.getMission(res)); | ||||
|                         DownloadManagerService.startMission( | ||||
|                                 DownloadActivity.this, | ||||
|                                 getIntent().getData().toString(), location, fName, | ||||
|                                 audioButton.isChecked(), threads.getProgress() + 1); | ||||
|                         mFragment.notifyChange(); | ||||
|  | ||||
|                         mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit(); | ||||
| @@ -277,4 +265,5 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O | ||||
|                         super.onOptionsItemSelected(item); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -26,6 +26,8 @@ import android.widget.TextView; | ||||
|  | ||||
| import org.schabi.newpipe.App; | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.extractor.NewPipe; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| @@ -65,24 +67,6 @@ public class DownloadDialog extends DialogFragment { | ||||
|     public static final String AUDIO_URL = "audio_url"; | ||||
|     public static final String VIDEO_URL = "video_url"; | ||||
|  | ||||
|     private DownloadManager mManager; | ||||
|     private DownloadManagerService.DMBinder mBinder; | ||||
|  | ||||
|     private ServiceConnection mConnection = new ServiceConnection() { | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceConnected(ComponentName p1, IBinder binder) { | ||||
|             mBinder = (DownloadManagerService.DMBinder) binder; | ||||
|             mManager = mBinder.getDownloadManager(); | ||||
|         } | ||||
|  | ||||
|         @Override | ||||
|         public void onServiceDisconnected(ComponentName p1) { | ||||
|  | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|  | ||||
|     public DownloadDialog() { | ||||
|  | ||||
|     } | ||||
| @@ -102,12 +86,6 @@ public class DownloadDialog extends DialogFragment { | ||||
|         if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) | ||||
|             ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0); | ||||
|  | ||||
|         Intent i = new Intent(); | ||||
|         i.setClass(getContext(), DownloadManagerService.class); | ||||
|         getContext().startService(i); | ||||
|         getContext().bindService(i, mConnection, Context.BIND_AUTO_CREATE); | ||||
|  | ||||
|  | ||||
|         return inflater.inflate(R.layout.dialog_url, container); | ||||
|     } | ||||
|  | ||||
| @@ -219,36 +197,22 @@ public class DownloadDialog extends DialogFragment { | ||||
|  | ||||
|         String fName = name.getText().toString().trim(); | ||||
|  | ||||
|         // todo: add timeout? would be bad if the thread gets locked dueto this. | ||||
|         while (mBinder == null); | ||||
|  | ||||
|         if(audioButton.isChecked()){ | ||||
|             int res = mManager.startMission( | ||||
|                     arguments.getString(AUDIO_URL), | ||||
|                     fName + arguments.getString(FILE_SUFFIX_AUDIO), | ||||
|                     audioButton.isChecked(), | ||||
|                     threads.getProgress() + 1); | ||||
|             DownloadMission mission = mManager.getMission(res); | ||||
|             mBinder.onMissionAdded(mission); | ||||
|             // add download listener to allow media scan notification | ||||
|             DownloadListener listener = new DownloadListener(getContext(), mission); | ||||
|             mission.addListener(listener); | ||||
|         boolean isAudio = audioButton.isChecked(); | ||||
|         String url, location, filename; | ||||
|         if(isAudio) { | ||||
|             url = arguments.getString(AUDIO_URL); | ||||
|             location = NewPipeSettings.getAudioDownloadPath(getContext()); | ||||
|             filename = fName + arguments.getString(FILE_SUFFIX_AUDIO); | ||||
|         } else { | ||||
|             url = arguments.getString(VIDEO_URL); | ||||
|             location = NewPipeSettings.getVideoDownloadPath(getContext()); | ||||
|             filename = fName + arguments.getString(FILE_SUFFIX_VIDEO); | ||||
|         } | ||||
|  | ||||
|         if(videoButton.isChecked()){ | ||||
|             int res = mManager.startMission( | ||||
|                     arguments.getString(VIDEO_URL), | ||||
|                     fName + arguments.getString(FILE_SUFFIX_VIDEO), | ||||
|                     audioButton.isChecked(), | ||||
|                     threads.getProgress() + 1); | ||||
|             DownloadMission mission = mManager.getMission(res); | ||||
|             mBinder.onMissionAdded(mission); | ||||
|             // add download listener to allow media scan notification | ||||
|             DownloadListener listener = new DownloadListener(getContext(), mission); | ||||
|             mission.addListener(listener); | ||||
|         } | ||||
|         DownloadManagerService.startMission(getContext(), url, location, filename, isAudio, | ||||
|                 threads.getProgress() + 1); | ||||
|  | ||||
|         getDialog().dismiss(); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private void download(String url, String title, | ||||
|   | ||||
| @@ -1,62 +0,0 @@ | ||||
| package org.schabi.newpipe.download; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.DownloadMission.MissionListener; | ||||
|  | ||||
| /** | ||||
|  * Created by erwin on 06.11.16. | ||||
|  * | ||||
|  * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> | ||||
|  * DownloadListener.java is part of NewPipe. | ||||
|  * | ||||
|  * NewPipe is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU General Public License as published by | ||||
|  * the Free Software Foundation, either version 3 of the License, or | ||||
|  * (at your option) any later version. | ||||
|  * | ||||
|  * NewPipe is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU General Public License | ||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| class DownloadListener implements MissionListener | ||||
| { | ||||
|     DownloadMission mMission; | ||||
|     Context mContext; | ||||
|  | ||||
|     public DownloadListener(Context context, DownloadMission mission) | ||||
|     { | ||||
|         super(); | ||||
|         mMission = mission; | ||||
|         mContext = context; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onProgressUpdate(long done, long total) | ||||
|     { | ||||
|         // do nothing special ... | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onFinish() | ||||
|     { | ||||
|         // notify media scanner on downloaded media file ... | ||||
|         mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, | ||||
|                                           Uri.parse( "file://" + mMission.location | ||||
|                                                      + "/" + mMission.name))); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onError(int errCode) | ||||
|     { | ||||
|         // do nothing special ... | ||||
|     } | ||||
| } | ||||
| @@ -42,7 +42,7 @@ import info.guardianproject.netcipher.NetCipher; | ||||
|  */ | ||||
|  | ||||
|  | ||||
| // TODO: FOR HEVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!! | ||||
| // TODO: FOR HEAVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!! | ||||
| public class FileDownloader extends AsyncTask<Void, Integer, Void> { | ||||
|     public static final String TAG = "FileDownloader"; | ||||
|  | ||||
|   | ||||
| @@ -88,7 +88,7 @@ public class YoutubeStreamExtractor extends StreamExtractor { | ||||
|     // $$el_type$$ will be replaced by the actual el_type (se the declarations below) | ||||
|     private static final String GET_VIDEO_INFO_URL = | ||||
|             "https://www.youtube.com/get_video_info?video_id=%%video_id%%$$el_type$$&ps=default&eurl=&gl=US&hl=en"; | ||||
|     // eltype is nececeary for the url aboth | ||||
|     // eltype is necessary for the url above | ||||
|     private static final String EL_INFO = "el=info"; | ||||
|  | ||||
|     public enum ItagType { | ||||
|   | ||||
| @@ -0,0 +1,68 @@ | ||||
| package org.schabi.newpipe.util; | ||||
|  | ||||
| import android.Manifest; | ||||
| import android.app.Activity; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Build; | ||||
| import android.support.annotation.RequiresApi; | ||||
| import android.support.v4.app.ActivityCompat; | ||||
| import android.support.v4.content.ContextCompat; | ||||
|  | ||||
| public class PermissionHelper { | ||||
|     public static final int PERMISSION_WRITE_STORAGE = 778; | ||||
|     public static final int PERMISSION_READ_STORAGE = 777; | ||||
|  | ||||
|  | ||||
|  | ||||
|     public static boolean checkStoragePermissions(Activity activity) { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { | ||||
|             if(!checkReadStoragePermissions(activity)) return false; | ||||
|         } | ||||
|         return checkWriteStoragePermissions(activity); | ||||
|     } | ||||
|  | ||||
|     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) | ||||
|     public static boolean checkReadStoragePermissions(Activity activity) { | ||||
|         if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) | ||||
|                 != PackageManager.PERMISSION_GRANTED) { | ||||
|             ActivityCompat.requestPermissions(activity, | ||||
|                     new String[]{ | ||||
|                             Manifest.permission.READ_EXTERNAL_STORAGE, | ||||
|                             Manifest.permission.WRITE_EXTERNAL_STORAGE}, | ||||
|                     PERMISSION_READ_STORAGE); | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     public static boolean checkWriteStoragePermissions(Activity activity) { | ||||
|         // Here, thisActivity is the current activity | ||||
|         if (ContextCompat.checkSelfPermission(activity, | ||||
|                 Manifest.permission.WRITE_EXTERNAL_STORAGE) | ||||
|                 != PackageManager.PERMISSION_GRANTED) { | ||||
|  | ||||
|             // Should we show an explanation? | ||||
|             /*if (ActivityCompat.shouldShowRequestPermissionRationale(activity, | ||||
|                     Manifest.permission.WRITE_EXTERNAL_STORAGE)) { | ||||
|  | ||||
|                 // Show an explanation to the user *asynchronously* -- don't block | ||||
|                 // this thread waiting for the user's response! After the user | ||||
|                 // sees the explanation, try again to request the permission. | ||||
|             } else {*/ | ||||
|  | ||||
|             // No explanation needed, we can request the permission. | ||||
|             ActivityCompat.requestPermissions(activity, | ||||
|                     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, | ||||
|                     PERMISSION_WRITE_STORAGE); | ||||
|  | ||||
|             // PERMISSION_WRITE_STORAGE is an | ||||
|             // app-defined int constant. The callback method gets the | ||||
|             // result of the request. | ||||
|             /*}*/ | ||||
|             return false; | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,36 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import java.util.List; | ||||
|  | ||||
| /** | ||||
|  * Provides access to the storage of {@link DownloadMission}s | ||||
|  */ | ||||
| public interface DownloadDataSource { | ||||
|  | ||||
|     /** | ||||
|      * Load all missions | ||||
|      * @return a list of download missions | ||||
|      */ | ||||
|     List<DownloadMission> loadMissions(); | ||||
|  | ||||
|     /** | ||||
|      * Add a downlaod mission to the storage | ||||
|      * @param downloadMission the download mission to add | ||||
|      * @return the identifier of the mission | ||||
|      */ | ||||
|     void addMission(DownloadMission downloadMission); | ||||
|  | ||||
|     /** | ||||
|      * Update a download mission which exists in the storage | ||||
|      * @param downloadMission the download mission to update | ||||
|      * @throws IllegalArgumentException if the mission was not added to storage | ||||
|      */ | ||||
|     void updateMission(DownloadMission downloadMission); | ||||
|  | ||||
|  | ||||
|     /** | ||||
|      * Delete a download mission | ||||
|      * @param downloadMission the mission to delete | ||||
|      */ | ||||
|     void deleteMission(DownloadMission downloadMission); | ||||
| } | ||||
| @@ -3,12 +3,46 @@ package us.shandian.giga.get; | ||||
| public interface DownloadManager | ||||
| { | ||||
| 	int BLOCK_SIZE = 512 * 1024; | ||||
| 	 | ||||
| 	int startMission(String url, String name, boolean isAudio, int threads); | ||||
|  | ||||
| 	/** | ||||
| 	 * Start a new download mission | ||||
| 	 * @param url the url to download | ||||
| 	 * @param location the location | ||||
| 	 * @param name the name of the file to create | ||||
| 	 * @param isAudio true if the download is an audio file | ||||
| 	 * @param threads the number of threads maximal used to download chunks of the file.    @return the identifier of the mission. | ||||
|      */ | ||||
| 	int startMission(String url, String location, String name, boolean isAudio, int threads); | ||||
|  | ||||
| 	/** | ||||
| 	 * Resume the execution of a download mission. | ||||
| 	 * @param id the identifier of the mission to resume. | ||||
| 	 */ | ||||
| 	void resumeMission(int id); | ||||
|  | ||||
| 	/** | ||||
| 	 * Pause the execution of a download mission. | ||||
| 	 * @param id the identifier of the mission to pause. | ||||
|      */ | ||||
| 	void pauseMission(int id); | ||||
|  | ||||
| 	/** | ||||
| 	 * Deletes the mission from the downloaded list but keeps the downloaded file. | ||||
| 	 * @param id The mission identifier | ||||
|      */ | ||||
| 	void deleteMission(int id); | ||||
|  | ||||
| 	/** | ||||
| 	 * Get the download mission by its identifier | ||||
| 	 * @param id the identifier of the download mission | ||||
| 	 * @return the download mission or null if the mission doesn't exist | ||||
|      */ | ||||
| 	DownloadMission getMission(int id); | ||||
|  | ||||
| 	/** | ||||
| 	 * Get the number of download missions. | ||||
| 	 * @return the number of download missions. | ||||
|      */ | ||||
| 	int getCount(); | ||||
| 	String getLocation(); | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,17 +1,21 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.Log; | ||||
|  | ||||
| import com.google.gson.Gson; | ||||
|  | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FilenameFilter; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
| import java.util.Collections; | ||||
| import java.util.Comparator; | ||||
| import java.util.List; | ||||
|  | ||||
| import us.shandian.giga.util.Utility; | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
| @@ -19,33 +23,48 @@ import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
| public class DownloadManagerImpl implements DownloadManager | ||||
| { | ||||
| 	private static final String TAG = DownloadManagerImpl.class.getSimpleName(); | ||||
| 	 | ||||
| 	private Context mContext; | ||||
| 	private String mLocation; | ||||
| 	protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>(); | ||||
| 	 | ||||
| 	public DownloadManagerImpl(Context context, String location) { | ||||
| 		mContext = context; | ||||
| 		mLocation = location; | ||||
| 		loadMissions(); | ||||
| 	private final DownloadDataSource mDownloadDataSource; | ||||
|  | ||||
| 	private final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>(); | ||||
|  | ||||
|     /** | ||||
|      * Create a new instance | ||||
|      * @param searchLocations the directories to search for unfinished downloads | ||||
|      * @param downloadDataSource the data source for finished downloads | ||||
|      */ | ||||
| 	public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) { | ||||
| 		mDownloadDataSource = downloadDataSource; | ||||
| 		loadMissions(searchLocations); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public int startMission(String url, String name, boolean isAudio, int threads) { | ||||
| 		DownloadMission mission = new DownloadMission(); | ||||
| 		mission.url = url; | ||||
| 		mission.name = name; | ||||
| 		if(isAudio) { | ||||
| 			mission.location = NewPipeSettings.getAudioDownloadPath(mContext); | ||||
| 		} else { | ||||
| 			mission.location = NewPipeSettings.getVideoDownloadPath(mContext); | ||||
| 	public int startMission(String url, String location, String name, boolean isAudio, int threads) { | ||||
| 		DownloadMission existingMission = getMissionByLocation(location, name); | ||||
| 		if(existingMission != null) { | ||||
| 			// Already downloaded or downloading | ||||
| 			if(existingMission.finished) { | ||||
| 				// Overwrite mission | ||||
|                 deleteMission(mMissions.indexOf(existingMission)); | ||||
| 			} else { | ||||
| 				// Rename file (?) | ||||
|                 try { | ||||
|                     name = generateUniqueName(location, name); | ||||
|                 }catch (Exception e) { | ||||
|                     Log.e(TAG, "Unable to generate unique name", e); | ||||
|                     name = System.currentTimeMillis() + name ; | ||||
|                     Log.i(TAG, "Using " + name); | ||||
|                 } | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		DownloadMission mission = new DownloadMission(name, url, location); | ||||
| 		mission.timestamp = System.currentTimeMillis(); | ||||
| 		mission.threadCount = threads; | ||||
| 		new Initializer(mContext, mission).start(); | ||||
| 		mission.addListener(new MissionListener(mission)); | ||||
| 		new Initializer(mission).start(); | ||||
| 		return insertMission(mission); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public void resumeMission(int i) { | ||||
| 		DownloadMission d = getMission(i); | ||||
| @@ -53,7 +72,7 @@ public class DownloadManagerImpl implements DownloadManager | ||||
| 			d.start(); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public void pauseMission(int i) { | ||||
| 		DownloadMission d = getMission(i); | ||||
| @@ -61,55 +80,94 @@ public class DownloadManagerImpl implements DownloadManager | ||||
| 			d.pause(); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public void deleteMission(int i) { | ||||
| 		getMission(i).delete(); | ||||
| 		DownloadMission mission = getMission(i); | ||||
| 		if(mission.finished) { | ||||
| 			mDownloadDataSource.deleteMission(mission); | ||||
| 		} | ||||
| 		mission.delete(); | ||||
| 		mMissions.remove(i); | ||||
| 	} | ||||
| 	 | ||||
| 	private void loadMissions() { | ||||
| 		File f = new File(mLocation); | ||||
|  | ||||
| 	private void loadMissions(Iterable<String> searchLocations) { | ||||
| 		mMissions.clear(); | ||||
| 		loadFinishedMissions(); | ||||
| 		for(String location: searchLocations) { | ||||
| 			loadMissions(location); | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	/** | ||||
| 	 * Loads finished missions from the data source | ||||
| 	 */ | ||||
| 	private void loadFinishedMissions() { | ||||
| 		List<DownloadMission> finishedMissions = mDownloadDataSource.loadMissions(); | ||||
|         if(finishedMissions == null) { | ||||
|             finishedMissions = new ArrayList<>(); | ||||
|         } | ||||
|         // Ensure its sorted | ||||
|         Collections.sort(finishedMissions, new Comparator<DownloadMission>() { | ||||
|             @Override | ||||
|             public int compare(DownloadMission o1, DownloadMission o2) { | ||||
|                 return (int) (o1.timestamp - o2.timestamp); | ||||
|             } | ||||
|         }); | ||||
|         mMissions.ensureCapacity(mMissions.size() + finishedMissions.size()); | ||||
| 		for(DownloadMission mission: finishedMissions) { | ||||
| 			File downloadedFile = mission.getDownloadedFile(); | ||||
| 			if(!downloadedFile.isFile()) { | ||||
| 				if(DEBUG) { | ||||
| 					Log.d(TAG, "downloaded file removed: " + downloadedFile.getAbsolutePath()); | ||||
| 				} | ||||
| 				mDownloadDataSource.deleteMission(mission); | ||||
| 			} else { | ||||
| 				mission.length = downloadedFile.length(); | ||||
| 				mission.finished = true; | ||||
| 				mission.running = false; | ||||
| 				mMissions.add(mission); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	private void loadMissions(String location) { | ||||
|  | ||||
| 		File f = new File(location); | ||||
|  | ||||
| 		if (f.exists() && f.isDirectory()) { | ||||
| 			File[] subs = f.listFiles(); | ||||
| 			 | ||||
|  | ||||
|             if(subs == null) { | ||||
|                 Log.e(TAG, "listFiles() returned null"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
| 			for (File sub : subs) { | ||||
| 				if (sub.isDirectory()) { | ||||
| 					continue; | ||||
| 				} | ||||
| 				 | ||||
| 				if (sub.getName().endsWith(".giga")) { | ||||
| 				if (sub.isFile() && sub.getName().endsWith(".giga")) { | ||||
| 					String str = Utility.readFromFile(sub.getAbsolutePath()); | ||||
| 					if (str != null && !str.trim().equals("")) { | ||||
| 						 | ||||
|  | ||||
| 						if (DEBUG) { | ||||
| 							Log.d(TAG, "loading mission " + sub.getName()); | ||||
| 							Log.d(TAG, str); | ||||
| 						} | ||||
| 						 | ||||
|  | ||||
| 						DownloadMission mis = new Gson().fromJson(str, DownloadMission.class); | ||||
| 						 | ||||
|  | ||||
| 						if (mis.finished) { | ||||
| 							sub.delete(); | ||||
| 							if(!sub.delete()) { | ||||
|                                 Log.w(TAG, "Unable to delete .giga file: " + sub.getPath()); | ||||
|                             } | ||||
| 							continue; | ||||
| 						} | ||||
| 						 | ||||
|  | ||||
| 						mis.running = false; | ||||
| 						mis.recovered = true; | ||||
| 						insertMission(mis); | ||||
| 					} | ||||
| 				} else if (!sub.getName().startsWith(".") && !new File(sub.getPath() + ".giga").exists()) { | ||||
| 					// Add a dummy mission for downloaded files | ||||
| 					DownloadMission mis = new DownloadMission(); | ||||
| 					mis.length = sub.length(); | ||||
| 					mis.done = mis.length; | ||||
| 					mis.finished = true; | ||||
| 					mis.running = false; | ||||
| 					mis.name = sub.getName(); | ||||
| 					mis.location = mLocation; | ||||
| 					mis.timestamp = sub.lastModified(); | ||||
| 					insertMission(mis); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| @@ -144,18 +202,81 @@ public class DownloadManagerImpl implements DownloadManager | ||||
| 		 | ||||
| 		return i; | ||||
| 	} | ||||
| 	 | ||||
| 	@Override | ||||
| 	public String getLocation() { | ||||
| 		return mLocation; | ||||
|  | ||||
|     /** | ||||
| 	 * Get a mission by its location and name | ||||
| 	 * @param location the location | ||||
| 	 * @param name the name | ||||
| 	 * @return the mission or null if no such mission exists | ||||
| 	 */ | ||||
| 	private @Nullable DownloadMission getMissionByLocation(String location, String name) { | ||||
| 		for(DownloadMission mission: mMissions) { | ||||
| 			if(location.equals(mission.location) && name.equals(mission.name)) { | ||||
| 				return mission; | ||||
| 			} | ||||
| 		} | ||||
| 		return null; | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Splits the filename into name and extension | ||||
| 	 * | ||||
| 	 * Dots are ignored if they appear: not at all, at the beginning of the file, | ||||
| 	 * at the end of the file | ||||
| 	 * | ||||
| 	 * @param name the name to split | ||||
| 	 * @return a string array with a length of 2 containing the name and the extension | ||||
|      */ | ||||
| 	private static String[] splitName(String name) { | ||||
| 		int dotIndex = name.lastIndexOf('.'); | ||||
| 		if(dotIndex <= 0 || (dotIndex  == name.length() - 1)) { | ||||
| 			return new String[]{name, ""}; | ||||
| 		} else { | ||||
| 			return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)}; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Generates a unique file name. | ||||
| 	 * | ||||
| 	 * e.g. "myname (1).txt" if the name "myname.txt" exists. | ||||
| 	 * @param location the location (to check for existing files) | ||||
| 	 * @param name the name of the file | ||||
|      * @return the unique file name | ||||
| 	 * @throws IllegalArgumentException if the location is not a directory | ||||
| 	 * @throws SecurityException if the location is not readable | ||||
|      */ | ||||
| 	private static String generateUniqueName(String location, String name) { | ||||
| 		if(location == null) throw new NullPointerException("location is null"); | ||||
| 		if(name == null) throw new NullPointerException("name is null"); | ||||
| 		File destination = new File(location); | ||||
| 		if(!destination.isDirectory()) { | ||||
| 			throw new IllegalArgumentException("location is not a directory: " + location); | ||||
| 		} | ||||
| 		final String[] nameParts = splitName(name); | ||||
| 		String[] existingName = destination.list(new FilenameFilter() { | ||||
| 			@Override | ||||
| 			public boolean accept(File dir, String name) { | ||||
| 				return name.startsWith(nameParts[0]); | ||||
| 			} | ||||
| 		}); | ||||
| 		Arrays.sort(existingName); | ||||
| 		String newName; | ||||
| 		int downloadIndex = 0; | ||||
| 		do { | ||||
| 			newName = nameParts[0] + " (" + downloadIndex + ")." + nameParts[1]; | ||||
| 			++downloadIndex; | ||||
| 			if(downloadIndex == 1000) {  // Probably an error on our side | ||||
| 				throw new RuntimeException("Too many existing files"); | ||||
| 			} | ||||
| 		} while (Arrays.binarySearch(existingName, newName) >= 0); | ||||
| 		return newName; | ||||
| 	} | ||||
|  | ||||
| 	private class Initializer extends Thread { | ||||
| 		private Context context; | ||||
| 		private DownloadMission mission; | ||||
| 		 | ||||
| 		public Initializer(Context context, DownloadMission mission) { | ||||
| 			this.context = context; | ||||
| 		public Initializer(DownloadMission mission) { | ||||
| 			this.mission = mission; | ||||
| 		} | ||||
| 		 | ||||
| @@ -217,4 +338,30 @@ public class DownloadManagerImpl implements DownloadManager | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Waits for mission to finish to add it to the {@link #mDownloadDataSource} | ||||
| 	 */ | ||||
| 	private class MissionListener implements DownloadMission.MissionListener { | ||||
| 		private final DownloadMission mMission; | ||||
|  | ||||
| 		private MissionListener(DownloadMission mission) { | ||||
| 			if(mission == null) throw new NullPointerException("mission is null"); | ||||
| 			// Could the mission be passed in onFinish()? | ||||
| 			mMission = mission; | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onFinish(DownloadMission downloadMission) { | ||||
| 			mDownloadDataSource.addMission(mMission); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onError(DownloadMission downloadMission, int errCode) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package us.shandian.giga.get; | ||||
|  | ||||
| import android.content.Context; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.util.Log; | ||||
| @@ -10,39 +9,63 @@ import com.google.gson.Gson; | ||||
| import java.io.File; | ||||
| import java.lang.ref.WeakReference; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Iterator; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
|  | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadMission | ||||
| { | ||||
| 	private static final String TAG = DownloadMission.class.getSimpleName(); | ||||
| 	 | ||||
|  | ||||
| 	public interface MissionListener { | ||||
| 		HashMap<MissionListener, Handler> handlerStore = new HashMap<>(); | ||||
| 		 | ||||
| 		void onProgressUpdate(long done, long total); | ||||
| 		void onFinish(); | ||||
| 		void onError(int errCode); | ||||
| 		void onProgressUpdate(DownloadMission downloadMission, long done, long total); | ||||
| 		void onFinish(DownloadMission downloadMission); | ||||
| 		void onError(DownloadMission downloadMission, int errCode); | ||||
| 	} | ||||
| 	 | ||||
| 	public static final int ERROR_SERVER_UNSUPPORTED = 206; | ||||
| 	public static final int ERROR_UNKNOWN = 233; | ||||
| 	 | ||||
| 	public String name = ""; | ||||
| 	public String url = ""; | ||||
| 	public String location = ""; | ||||
|  | ||||
| 	/** | ||||
| 	 * The filename | ||||
| 	 */ | ||||
| 	public String name; | ||||
|  | ||||
| 	/** | ||||
| 	 * The url of the file to download | ||||
| 	 */ | ||||
| 	public String url; | ||||
|  | ||||
| 	/** | ||||
| 	 * The directory to store the download | ||||
| 	 */ | ||||
| 	public String location; | ||||
|  | ||||
| 	/** | ||||
| 	 * Number of blocks the size of {@link DownloadManager#BLOCK_SIZE} | ||||
| 	 */ | ||||
| 	public long blocks; | ||||
|  | ||||
| 	/** | ||||
| 	 * Number of bytes | ||||
| 	 */ | ||||
| 	public long length; | ||||
|  | ||||
| 	/** | ||||
| 	 * Number of bytes downloaded | ||||
| 	 */ | ||||
| 	public long done; | ||||
| 	public int threadCount = 3; | ||||
| 	public int finishCount; | ||||
| 	public List<Long> threadPositions = new ArrayList<Long>(); | ||||
| 	public Map<Long, Boolean> blockState = new HashMap<Long, Boolean>(); | ||||
| 	private List<Long> threadPositions = new ArrayList<Long>(); | ||||
| 	public final Map<Long, Boolean> blockState = new HashMap<Long, Boolean>(); | ||||
| 	public boolean running; | ||||
| 	public boolean finished; | ||||
| 	public boolean fallback; | ||||
| @@ -53,23 +76,65 @@ public class DownloadMission | ||||
| 	 | ||||
| 	private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>(); | ||||
| 	private transient boolean mWritingToFile; | ||||
| 	 | ||||
|  | ||||
| 	private static final int NO_IDENTIFIER = -1; | ||||
| 	private long db_identifier = NO_IDENTIFIER; | ||||
|  | ||||
| 	public DownloadMission() { | ||||
| 	} | ||||
|  | ||||
| 	public DownloadMission(String name, String url, String location) { | ||||
| 		if(name == null) throw new NullPointerException("name is null"); | ||||
| 		if(name.isEmpty()) throw new IllegalArgumentException("name is empty"); | ||||
| 		if(url == null) throw new NullPointerException("url is null"); | ||||
| 		if(url.isEmpty()) throw new IllegalArgumentException("url is empty"); | ||||
| 		if(location == null) throw new NullPointerException("location is null"); | ||||
| 		if(location.isEmpty()) throw new IllegalArgumentException("location is empty"); | ||||
| 		this.url = url; | ||||
| 		this.name = name; | ||||
| 		this.location = location; | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	private void checkBlock(long block) { | ||||
| 		if(block < 0 || block >= blocks) { | ||||
| 			throw new IllegalArgumentException("illegal block identifier"); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Check if a block is reserved | ||||
| 	 * @param block the block identifier | ||||
| 	 * @return true if the block is reserved and false if otherwise | ||||
|      */ | ||||
| 	public boolean isBlockPreserved(long block) { | ||||
| 		checkBlock(block); | ||||
| 		return blockState.containsKey(block) ? blockState.get(block) : false; | ||||
| 	} | ||||
| 	 | ||||
| 	public void preserveBlock(long block) { | ||||
| 		checkBlock(block); | ||||
| 		synchronized (blockState) { | ||||
| 			blockState.put(block, true); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	public void setPosition(int id, long position) { | ||||
| 		threadPositions.set(id, position); | ||||
|  | ||||
| 	/** | ||||
| 	 * Set the download position of the file | ||||
| 	 * @param threadId the identifier of the thread | ||||
| 	 * @param position the download position of the thread | ||||
|      */ | ||||
| 	public void setPosition(int threadId, long position) { | ||||
| 		threadPositions.set(threadId, position); | ||||
| 	} | ||||
| 	 | ||||
| 	public long getPosition(int id) { | ||||
| 		return threadPositions.get(id); | ||||
|  | ||||
| 	/** | ||||
| 	 * Get the position of a thread | ||||
| 	 * @param threadId the identifier of the thread | ||||
| 	 * @return the position for the thread | ||||
|      */ | ||||
| 	public long getPosition(int threadId) { | ||||
| 		return threadPositions.get(threadId); | ||||
| 	} | ||||
| 	 | ||||
| 	public synchronized void notifyProgress(long deltaLen) { | ||||
| @@ -95,13 +160,16 @@ public class DownloadMission | ||||
| 				MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
| 					@Override | ||||
| 					public void run() { | ||||
| 						listener.onProgressUpdate(done, length); | ||||
| 						listener.onProgressUpdate(DownloadMission.this, done, length); | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Called by a download thread when it finished. | ||||
| 	 */ | ||||
| 	public synchronized void notifyFinished() { | ||||
| 		if (errCode > 0) return; | ||||
| 		 | ||||
| @@ -111,7 +179,10 @@ public class DownloadMission | ||||
| 			onFinish(); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Called when all parts are downloaded | ||||
| 	 */ | ||||
| 	private void onFinish() { | ||||
| 		if (errCode > 0) return; | ||||
| 		 | ||||
| @@ -130,7 +201,7 @@ public class DownloadMission | ||||
| 				MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
| 					@Override | ||||
| 					public void run() { | ||||
| 						listener.onFinish(); | ||||
| 						listener.onFinish(DownloadMission.this); | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| @@ -147,7 +218,7 @@ public class DownloadMission | ||||
| 			MissionListener.handlerStore.get(listener).post(new Runnable() { | ||||
| 				@Override | ||||
| 				public void run() { | ||||
| 					listener.onError(errCode); | ||||
| 					listener.onError(DownloadMission.this, errCode); | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| @@ -169,7 +240,10 @@ public class DownloadMission | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Start downloading with multiple threads. | ||||
| 	 */ | ||||
| 	public void start() { | ||||
| 		if (!running && !finished) { | ||||
| 			running = true; | ||||
| @@ -200,12 +274,19 @@ public class DownloadMission | ||||
| 			// if (err) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Removes the file and the meta file | ||||
| 	 */ | ||||
| 	public void delete() { | ||||
| 		deleteThisFromFile(); | ||||
| 		new File(location + "/" + name).delete(); | ||||
| 		new File(location, name).delete(); | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Write this {@link DownloadMission} to the meta file asynchronously | ||||
| 	 * if no thread is already running. | ||||
| 	 */ | ||||
| 	public void writeThisToFile() { | ||||
| 		if (!mWritingToFile) { | ||||
| 			mWritingToFile = true; | ||||
| @@ -218,14 +299,30 @@ public class DownloadMission | ||||
| 			}.start(); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Write this {@link DownloadMission} to the meta file. | ||||
| 	 */ | ||||
| 	private void doWriteThisToFile() { | ||||
| 		synchronized (blockState) { | ||||
| 			Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this)); | ||||
| 			Utility.writeToFile(getMetaFilename(), new Gson().toJson(this)); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	private void deleteThisFromFile() { | ||||
| 		new File(location + "/" + name + ".giga").delete(); | ||||
| 		new File(getMetaFilename()).delete(); | ||||
| 	} | ||||
|  | ||||
| 	/** | ||||
| 	 * Get the path of the meta file | ||||
| 	 * @return the path to the meta file | ||||
|      */ | ||||
| 	private String getMetaFilename() { | ||||
| 		return location + "/" + name + ".giga"; | ||||
| 	} | ||||
|  | ||||
| 	public File getDownloadedFile() { | ||||
| 		return new File(location, name); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -9,14 +9,19 @@ import java.net.URL; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| /** | ||||
|  * Runnable to download blocks of a file until the file is completely downloaded, | ||||
|  * an error occurs or the process is stopped. | ||||
|  */ | ||||
| public class DownloadRunnable implements Runnable | ||||
| { | ||||
| 	private static final String TAG = DownloadRunnable.class.getSimpleName(); | ||||
| 	 | ||||
| 	private DownloadMission mMission; | ||||
| 	private int mId; | ||||
| 	private final DownloadMission mMission; | ||||
| 	private final int mId; | ||||
| 	 | ||||
| 	public DownloadRunnable(DownloadMission mission, int id) { | ||||
| 		if(mission == null) throw new NullPointerException("mission is null"); | ||||
| 		mMission = mission; | ||||
| 		mId = id; | ||||
| 	} | ||||
| @@ -86,7 +91,7 @@ public class DownloadRunnable implements Runnable | ||||
| 					Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); | ||||
| 				} | ||||
| 				 | ||||
| 				// A server may be ignoring the range requet | ||||
| 				// A server may be ignoring the range request | ||||
| 				if (conn.getResponseCode() != 206) { | ||||
| 					mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; | ||||
| 					notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); | ||||
| @@ -131,7 +136,7 @@ public class DownloadRunnable implements Runnable | ||||
| 				notifyProgress(-total); | ||||
| 				 | ||||
| 				if (DEBUG) { | ||||
| 					Log.d(TAG, mId + ":position " + position + " retrying"); | ||||
| 					Log.d(TAG, mId + ":position " + position + " retrying", e); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -8,10 +8,11 @@ import java.net.URL; | ||||
| // Single-threaded fallback mode | ||||
| public class DownloadRunnableFallback implements Runnable | ||||
| { | ||||
| 	private DownloadMission mMission; | ||||
| 	private final DownloadMission mMission; | ||||
| 	//private int mId; | ||||
| 	 | ||||
| 	public DownloadRunnableFallback(DownloadMission mission) { | ||||
| 		if(mission == null) throw new NullPointerException("mission is null"); | ||||
| 		//mId = id; | ||||
| 		mMission = mission; | ||||
| 	} | ||||
| @@ -35,7 +36,7 @@ public class DownloadRunnableFallback implements Runnable | ||||
| 					f.write(buf, 0, len); | ||||
| 					notifyProgress(len); | ||||
| 					 | ||||
| 					if (Thread.currentThread().interrupted()) { | ||||
| 					if (Thread.interrupted()) { | ||||
| 						break; | ||||
| 					} | ||||
| 					 | ||||
|   | ||||
| @@ -0,0 +1,102 @@ | ||||
| package us.shandian.giga.get.sqlite; | ||||
|  | ||||
| import android.content.ContentValues; | ||||
| import android.content.Context; | ||||
| import android.database.Cursor; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.database.sqlite.SQLiteOpenHelper; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| /** | ||||
|  * SqliteHelper to store {@link us.shandian.giga.get.DownloadMission} | ||||
|  */ | ||||
| public class DownloadMissionSQLiteHelper extends SQLiteOpenHelper { | ||||
|  | ||||
|  | ||||
|     private final String TAG = "DownloadMissionHelper"; | ||||
|  | ||||
|     // TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?) | ||||
|     private static final String DATABASE_NAME = "newpipe.db"; | ||||
|  | ||||
|     private static final int DATABASE_VERSION = 2; | ||||
|     /** | ||||
|      * The table name of download missions | ||||
|      */ | ||||
|     static final String MISSIONS_TABLE_NAME = "download_missions"; | ||||
|  | ||||
|     /** | ||||
|      * The key to the directory location of the mission | ||||
|      */ | ||||
|     static final String KEY_LOCATION = "location"; | ||||
|     /** | ||||
|      * The key to the url of a mission | ||||
|      */ | ||||
|     static final String KEY_URL = "url"; | ||||
|     /** | ||||
|      * The key to the name of a mission | ||||
|      */ | ||||
|     static final String KEY_NAME = "name"; | ||||
|  | ||||
|     /** | ||||
|      * The key to the done. | ||||
|      */ | ||||
|     static final String KEY_DONE = "bytes_downloaded"; | ||||
|  | ||||
|     static final String KEY_TIMESTAMP = "timestamp"; | ||||
|  | ||||
|     /** | ||||
|      * The statement to create the table | ||||
|      */ | ||||
|     private static final String MISSIONS_CREATE_TABLE = | ||||
|             "CREATE TABLE " + MISSIONS_TABLE_NAME + " (" + | ||||
|                     KEY_LOCATION + " TEXT NOT NULL, " + | ||||
|                     KEY_NAME + " TEXT NOT NULL, " + | ||||
|                     KEY_URL + " TEXT NOT NULL, " + | ||||
|                     KEY_DONE + " INTEGER NOT NULL, " + | ||||
|                     KEY_TIMESTAMP + " INTEGER NOT NULL, " + | ||||
|                     " UNIQUE(" + KEY_LOCATION + ", " + KEY_NAME + "));"; | ||||
|  | ||||
|  | ||||
|     DownloadMissionSQLiteHelper(Context context) { | ||||
|         super(context, DATABASE_NAME, null, DATABASE_VERSION); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns all values of the download mission as ContentValues. | ||||
|      * @param downloadMission the download mission | ||||
|      * @return the content values | ||||
|      */ | ||||
|     public static ContentValues getValuesOfMission(DownloadMission downloadMission) { | ||||
|         ContentValues values = new ContentValues(); | ||||
|         values.put(KEY_URL, downloadMission.url); | ||||
|         values.put(KEY_LOCATION, downloadMission.location); | ||||
|         values.put(KEY_NAME, downloadMission.name); | ||||
|         values.put(KEY_DONE, downloadMission.done); | ||||
|         values.put(KEY_TIMESTAMP, downloadMission.timestamp); | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onCreate(SQLiteDatabase db) { | ||||
|         db.execSQL(MISSIONS_CREATE_TABLE); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { | ||||
|         // Currently nothing to do | ||||
|     } | ||||
|  | ||||
|     public static DownloadMission getMissionFromCursor(Cursor cursor) { | ||||
|         if(cursor == null) throw new NullPointerException("cursor is null"); | ||||
|         int pos; | ||||
|         String name = cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME)); | ||||
|         String location = cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)); | ||||
|         String url = cursor.getString(cursor.getColumnIndexOrThrow(KEY_URL)); | ||||
|         DownloadMission mission = new DownloadMission(name, url, location); | ||||
|         mission.done = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE)); | ||||
|         mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP)); | ||||
|         mission.finished = true; | ||||
|         return mission; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,79 @@ | ||||
| package us.shandian.giga.get.sqlite; | ||||
|  | ||||
| import android.content.ContentValues; | ||||
| import android.content.Context; | ||||
| import android.database.Cursor; | ||||
| import android.database.sqlite.SQLiteDatabase; | ||||
| import android.util.Log; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadDataSource; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_LOCATION; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.KEY_NAME; | ||||
| import static us.shandian.giga.get.sqlite.DownloadMissionSQLiteHelper.MISSIONS_TABLE_NAME; | ||||
|  | ||||
|  | ||||
| /** | ||||
|  * Non-thread-safe implementation of {@link DownloadDataSource} | ||||
|  */ | ||||
| public class SQLiteDownloadDataSource implements DownloadDataSource { | ||||
|  | ||||
|     private static final String TAG = "DownloadDataSourceImpl"; | ||||
|     private final DownloadMissionSQLiteHelper downloadMissionSQLiteHelper; | ||||
|  | ||||
|     public SQLiteDownloadDataSource(Context context) { | ||||
|         downloadMissionSQLiteHelper = new DownloadMissionSQLiteHelper(context); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<DownloadMission> loadMissions() { | ||||
|         ArrayList<DownloadMission> result; | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getReadableDatabase(); | ||||
|         Cursor cursor = database.query(MISSIONS_TABLE_NAME, null, null, | ||||
|                 null, null, null, DownloadMissionSQLiteHelper.KEY_TIMESTAMP); | ||||
|  | ||||
|         int count = cursor.getCount(); | ||||
|         if(count == 0) return new ArrayList<>(); | ||||
|         result = new ArrayList<>(count); | ||||
|         while (cursor.moveToNext()) { | ||||
|             result.add(DownloadMissionSQLiteHelper.getMissionFromCursor(cursor)); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void addMission(DownloadMission downloadMission) { | ||||
|         if(downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission); | ||||
|         database.insert(MISSIONS_TABLE_NAME, null, values); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateMission(DownloadMission downloadMission) { | ||||
|         if(downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         ContentValues values = DownloadMissionSQLiteHelper.getValuesOfMission(downloadMission); | ||||
|         String whereClause = KEY_LOCATION+ " = ? AND " + | ||||
|                 KEY_NAME + " = ?"; | ||||
|         int rowsAffected = database.update(MISSIONS_TABLE_NAME, values, | ||||
|                 whereClause, new String[]{downloadMission.location, downloadMission.name}); | ||||
|         if(rowsAffected != 1) { | ||||
|             Log.e(TAG, "Expected 1 row to be affected by update but got " + rowsAffected); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void deleteMission(DownloadMission downloadMission) { | ||||
|         if(downloadMission == null) throw new NullPointerException("downloadMission is null"); | ||||
|         SQLiteDatabase database = downloadMissionSQLiteHelper.getWritableDatabase(); | ||||
|         database.delete(MISSIONS_TABLE_NAME, | ||||
|                 KEY_LOCATION + " = ? AND " + | ||||
|                 KEY_NAME + " = ?", | ||||
|                 new String[]{downloadMission.location, downloadMission.name}); | ||||
|     } | ||||
| } | ||||
| @@ -4,38 +4,69 @@ import android.Manifest; | ||||
| import android.app.Notification; | ||||
| import android.app.PendingIntent; | ||||
| import android.app.Service; | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.content.ServiceConnection; | ||||
| import android.graphics.drawable.BitmapDrawable; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.net.Uri; | ||||
| import android.os.Binder; | ||||
| import android.os.Handler; | ||||
| import android.os.HandlerThread; | ||||
| import android.os.IBinder; | ||||
| import android.os.Message; | ||||
| import android.support.v4.app.NotificationCompat.Builder; | ||||
| import android.support.v4.content.ContextCompat; | ||||
| import android.support.v4.content.PermissionChecker; | ||||
| import android.util.Log; | ||||
| import android.widget.Toast; | ||||
|  | ||||
| import org.schabi.newpipe.R; | ||||
| import org.schabi.newpipe.download.DownloadActivity; | ||||
| import org.schabi.newpipe.settings.NewPipeSettings; | ||||
| import org.schabi.newpipe.R; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadDataSource; | ||||
| import us.shandian.giga.get.DownloadManager; | ||||
| import us.shandian.giga.get.DownloadManagerImpl; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
| import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource; | ||||
|  | ||||
| import static org.schabi.newpipe.BuildConfig.DEBUG; | ||||
|  | ||||
| public class DownloadManagerService extends Service implements DownloadMission.MissionListener | ||||
| public class DownloadManagerService extends Service | ||||
| { | ||||
| 	 | ||||
|  | ||||
| 	private static final String TAG = DownloadManagerService.class.getSimpleName(); | ||||
| 	 | ||||
|  | ||||
| 	/** | ||||
| 	 * Message code of update messages stored as {@link Message#what}. | ||||
| 	 */ | ||||
| 	private static final int UPDATE_MESSAGE = 0; | ||||
| 	private static final int NOTIFICATION_ID = 1000; | ||||
| 	private static final String EXTRA_NAME = "DownloadManagerService.extra.name"; | ||||
| 	private static final String EXTRA_LOCATION = "DownloadManagerService.extra.location"; | ||||
| 	private static final String EXTRA_IS_AUDIO = "DownloadManagerService.extra.is_audio"; | ||||
| 	private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; | ||||
|  | ||||
|  | ||||
| 	private DMBinder mBinder; | ||||
| 	private DownloadManager mManager; | ||||
| 	private Notification mNotification; | ||||
| 	private Handler mHandler; | ||||
| 	private long mLastTimeStamp = System.currentTimeMillis(); | ||||
| 	private DownloadDataSource mDataSource; | ||||
|  | ||||
|  | ||||
|  | ||||
| 	private MissionListener missionListener = new MissionListener(); | ||||
|  | ||||
|  | ||||
| 	private void notifyMediaScanner(DownloadMission mission) { | ||||
| 		Uri uri = Uri.parse("file://" + mission.location + "/" + mission.name); | ||||
| 		// notify media scanner on downloaded media file ... | ||||
| 		sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void onCreate() { | ||||
| @@ -44,14 +75,19 @@ public class DownloadManagerService extends Service implements DownloadMission.M | ||||
| 		if (DEBUG) { | ||||
| 			Log.d(TAG, "onCreate"); | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 		mBinder = new DMBinder(); | ||||
| 		if(mDataSource == null) { | ||||
| 			mDataSource = new SQLiteDownloadDataSource(this); | ||||
| 		} | ||||
| 		if (mManager == null) { | ||||
| 			String path = NewPipeSettings.getVideoDownloadPath(this); | ||||
| 			mManager = new DownloadManagerImpl(this, path); | ||||
| 			ArrayList<String> paths = new ArrayList<>(2); | ||||
| 			paths.add(NewPipeSettings.getVideoDownloadPath(this)); | ||||
| 			paths.add(NewPipeSettings.getAudioDownloadPath(this)); | ||||
| 			mManager = new DownloadManagerImpl(paths, mDataSource); | ||||
| 			if (DEBUG) { | ||||
| 				Log.d(TAG, "mManager == null"); | ||||
| 				Log.d(TAG, "Download directory: " + path); | ||||
| 				Log.d(TAG, "Download directory: " + paths); | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| @@ -87,28 +123,50 @@ public class DownloadManagerService extends Service implements DownloadMission.M | ||||
| 		mHandler = new Handler(thread.getLooper()) { | ||||
| 			@Override | ||||
| 			public void handleMessage(Message msg) { | ||||
| 				if (msg.what == 0) { | ||||
| 					int runningCount = 0; | ||||
| 					 | ||||
| 					for (int i = 0; i < mManager.getCount(); i++) { | ||||
| 						if (mManager.getMission(i).running) { | ||||
| 							runningCount++; | ||||
| 				switch (msg.what) { | ||||
| 					case UPDATE_MESSAGE: { | ||||
| 						int runningCount = 0; | ||||
|  | ||||
| 						for (int i = 0; i < mManager.getCount(); i++) { | ||||
| 							if (mManager.getMission(i).running) { | ||||
| 								runningCount++; | ||||
| 							} | ||||
| 						} | ||||
| 						updateState(runningCount); | ||||
| 						break; | ||||
| 					} | ||||
| 					 | ||||
| 					updateState(runningCount); | ||||
| 				} | ||||
| 			} | ||||
| 		}; | ||||
| 		 | ||||
| 	} | ||||
|  | ||||
| 	private void startMissionAsync(final String url, final String location, final String name, | ||||
| 								   final boolean isAudio, final int threads) { | ||||
| 		mHandler.post(new Runnable() { | ||||
| 			@Override | ||||
| 			public void run() { | ||||
| 				int missionId = mManager.startMission(url, location, name, isAudio, threads); | ||||
| 				mBinder.onMissionAdded(mManager.getMission(missionId)); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public int onStartCommand(Intent intent, int flags, int startId) { | ||||
| 		if (DEBUG) { | ||||
| 			Log.d(TAG, "Starting"); | ||||
| 		} | ||||
|  | ||||
| 		Log.i(TAG, "Got intent: " + intent); | ||||
| 		String action = intent.getAction(); | ||||
| 		if(action != null && action.equals(Intent.ACTION_RUN)) { | ||||
| 			String name = intent.getStringExtra(EXTRA_NAME); | ||||
| 			String location = intent.getStringExtra(EXTRA_LOCATION); | ||||
| 			int threads = intent.getIntExtra(EXTRA_THREADS, 1); | ||||
| 			boolean isAudio = intent.getBooleanExtra(EXTRA_IS_AUDIO, false); | ||||
| 			String url = intent.getDataString(); | ||||
| 			startMissionAsync(url, location, name, isAudio, threads); | ||||
| 		} | ||||
| 		return START_NOT_STICKY; | ||||
| 	} | ||||
|  | ||||
| @@ -123,65 +181,76 @@ public class DownloadManagerService extends Service implements DownloadMission.M | ||||
| 		for (int i = 0; i < mManager.getCount(); i++) { | ||||
| 			mManager.pauseMission(i); | ||||
| 		} | ||||
| 		 | ||||
|  | ||||
| 		stopForeground(true); | ||||
| 	} | ||||
| 	 | ||||
| 	@Override | ||||
| 	public IBinder onBind(Intent intent) { | ||||
| 		int permissionCheck; | ||||
| 		if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { | ||||
| 			int permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); | ||||
| 			permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); | ||||
| 			if(permissionCheck == PermissionChecker.PERMISSION_DENIED) { | ||||
| 				Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show(); | ||||
| 				return null; | ||||
| 			} | ||||
| 			permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); | ||||
| 			if(permissionCheck == PermissionChecker.PERMISSION_DENIED) { | ||||
| 				Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); | ||||
| 				return null; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); | ||||
| 		if(permissionCheck == PermissionChecker.PERMISSION_DENIED) { | ||||
| 			Toast.makeText(this, "Permission denied (write)", Toast.LENGTH_SHORT).show(); | ||||
| 		} | ||||
|  | ||||
| 		return mBinder; | ||||
| 	} | ||||
| 	 | ||||
|  | ||||
| 	@Override | ||||
| 	public void onProgressUpdate(long done, long total) { | ||||
|  | ||||
| 		long now = System.currentTimeMillis(); | ||||
|  | ||||
| 		long delta = now - mLastTimeStamp; | ||||
|  | ||||
| 		if (delta > 2000) { | ||||
| 			postUpdateMessage(); | ||||
| 			mLastTimeStamp = now; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void onFinish() { | ||||
| 		postUpdateMessage(); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void onError(int errCode) { | ||||
| 		postUpdateMessage(); | ||||
| 	} | ||||
| 	 | ||||
| 	private void postUpdateMessage() { | ||||
| 		mHandler.sendEmptyMessage(0); | ||||
| 		mHandler.sendEmptyMessage(UPDATE_MESSAGE); | ||||
| 	} | ||||
| 	 | ||||
| 	private void updateState(int runningCount) { | ||||
| 		if (runningCount == 0) { | ||||
| 			stopForeground(true); | ||||
| 		} else { | ||||
| 			startForeground(1000, mNotification); | ||||
| 			startForeground(NOTIFICATION_ID, mNotification); | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	 | ||||
|  | ||||
| 	public static void startMission(Context context, String url, String location, String name, boolean isAudio, int threads) { | ||||
| 		Intent intent = new Intent(context, DownloadManagerService.class); | ||||
| 		intent.setAction(Intent.ACTION_RUN); | ||||
| 		intent.setData(Uri.parse(url)); | ||||
| 		intent.putExtra(EXTRA_NAME, name); | ||||
| 		intent.putExtra(EXTRA_LOCATION, location); | ||||
| 		intent.putExtra(EXTRA_IS_AUDIO, isAudio); | ||||
| 		intent.putExtra(EXTRA_THREADS, threads); | ||||
| 		context.startService(intent); | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	class MissionListener implements DownloadMission.MissionListener { | ||||
| 		@Override | ||||
| 		public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { | ||||
| 			long now = System.currentTimeMillis(); | ||||
| 			long delta = now - mLastTimeStamp; | ||||
| 			if (delta > 2000) { | ||||
| 				postUpdateMessage(); | ||||
| 				mLastTimeStamp = now; | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onFinish(DownloadMission downloadMission) { | ||||
| 			postUpdateMessage(); | ||||
| 			notifyMediaScanner(downloadMission); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onError(DownloadMission downloadMission, int errCode) { | ||||
| 			postUpdateMessage(); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|  | ||||
| 	// Wrapper of DownloadManager | ||||
| 	public class DMBinder extends Binder { | ||||
| 		public DownloadManager getDownloadManager() { | ||||
| @@ -189,15 +258,13 @@ public class DownloadManagerService extends Service implements DownloadMission.M | ||||
| 		} | ||||
| 		 | ||||
| 		public void onMissionAdded(DownloadMission mission) { | ||||
| 			mission.addListener(DownloadManagerService.this); | ||||
| 			mission.addListener(missionListener); | ||||
| 			postUpdateMessage(); | ||||
| 		} | ||||
| 		 | ||||
| 		public void onMissionRemoved(DownloadMission mission) { | ||||
| 			mission.removeListener(DownloadManagerService.this); | ||||
| 			mission.removeListener(missionListener); | ||||
| 			postUpdateMessage(); | ||||
| 		} | ||||
| 		 | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,9 @@ import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.AsyncTask; | ||||
| import android.os.Build; | ||||
| import android.support.v4.content.FileProvider; | ||||
| import android.util.Log; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.Menu; | ||||
| import android.view.MenuItem; | ||||
| @@ -28,10 +31,14 @@ import us.shandian.giga.service.DownloadManagerService; | ||||
| import us.shandian.giga.ui.common.ProgressDrawable; | ||||
| import us.shandian.giga.util.Utility; | ||||
|  | ||||
| import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; | ||||
| import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; | ||||
|  | ||||
| public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHolder> | ||||
| { | ||||
| 	private static final Map<Integer, String> ALGORITHMS = new HashMap<>(); | ||||
| 	 | ||||
| 	private static final String TAG = "MissionAdapter"; | ||||
|  | ||||
| 	static { | ||||
| 		ALGORITHMS.put(R.id.md5, "MD5"); | ||||
| 		ALGORITHMS.put(R.id.sha1, "SHA1"); | ||||
| @@ -212,23 +219,23 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
| 						h.lastDone = -1; | ||||
| 						return true; | ||||
| 					case R.id.view: | ||||
| 						Intent i = new Intent(); | ||||
| 						i.setAction(Intent.ACTION_VIEW); | ||||
| 						File f = new File(h.mission.location + "/" + h.mission.name); | ||||
| 						File f = new File(h.mission.location, h.mission.name); | ||||
| 						String ext = Utility.getFileExt(h.mission.name); | ||||
| 						 | ||||
| 						if (ext == null) return false; | ||||
|  | ||||
| 						Log.d(TAG, "Viewing file: " + f.getAbsolutePath() + " ext: " + ext); | ||||
|  | ||||
| 						if (ext == null) { | ||||
| 							Log.w(TAG, "Can't view file because it has no extension: " + | ||||
| 									h.mission.name); | ||||
| 							return false; | ||||
| 						} | ||||
| 						 | ||||
| 						String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1)); | ||||
| 						 | ||||
| 						Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider"); | ||||
| 						if (f.exists()) { | ||||
| 							i.setDataAndType(Uri.fromFile(f), mime); | ||||
| 							 | ||||
| 							try { | ||||
| 								mContext.startActivity(i); | ||||
| 							} catch (Exception e) { | ||||
| 								 | ||||
| 							} | ||||
| 							viewFileWithFileProvider(f, mime); | ||||
| 						} else { | ||||
| 							Log.w(TAG, "File doesn't exist"); | ||||
| 						} | ||||
| 						 | ||||
| 						return true; | ||||
| @@ -249,6 +256,34 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
| 		 | ||||
| 		popup.show(); | ||||
| 	} | ||||
|  | ||||
| 	private void viewFile(File file, String mimetype) { | ||||
| 		Intent intent = new Intent(); | ||||
| 		intent.setAction(Intent.ACTION_VIEW); | ||||
| 		intent.setDataAndType(Uri.fromFile(file), mimetype); | ||||
| 		intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); | ||||
| 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
| 			intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); | ||||
| 		} | ||||
| 		//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
| 		Log.v(TAG, "Starting intent: " + intent); | ||||
| 		mContext.startActivity(intent); | ||||
| 	} | ||||
|  | ||||
| 	private void viewFileWithFileProvider(File file, String mimetype) { | ||||
| 		String ourPackage = mContext.getApplicationContext().getPackageName(); | ||||
| 		Uri uri = FileProvider.getUriForFile(mContext,  ourPackage + ".provider", file); | ||||
| 		Intent intent = new Intent(); | ||||
| 		intent.setAction(Intent.ACTION_VIEW); | ||||
| 		intent.setDataAndType(uri, mimetype); | ||||
| 		intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); | ||||
| 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
| 			intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); | ||||
| 		} | ||||
| 		//mContext.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||||
| 		Log.v(TAG, "Starting intent: " + intent); | ||||
| 		mContext.startActivity(intent); | ||||
| 	} | ||||
| 	 | ||||
| 	private class ChecksumTask extends AsyncTask<String, Void, String> { | ||||
| 		ProgressDialog prog; | ||||
| @@ -316,12 +351,12 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
| 		} | ||||
| 		 | ||||
| 		@Override | ||||
| 		public void onProgressUpdate(long done, long total) { | ||||
| 		public void onProgressUpdate(DownloadMission downloadMission, long done, long total) { | ||||
| 			mAdapter.updateProgress(mHolder); | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onFinish() { | ||||
| 		public void onFinish(DownloadMission downloadMission) { | ||||
| 			//mAdapter.mManager.deleteMission(mHolder.position); | ||||
| 			// TODO Notification | ||||
| 			//mAdapter.notifyDataSetChanged(); | ||||
| @@ -332,7 +367,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold | ||||
| 		} | ||||
|  | ||||
| 		@Override | ||||
| 		public void onError(int errCode) { | ||||
| 		public void onError(DownloadMission downloadMission, int errCode) { | ||||
| 			mAdapter.updateProgress(mHolder); | ||||
| 		} | ||||
| 		 | ||||
|   | ||||
| @@ -40,7 +40,7 @@ public abstract class MissionsFragment extends Fragment | ||||
| 	private MissionAdapter mAdapter; | ||||
| 	private GridLayoutManager mGridManager; | ||||
| 	private LinearLayoutManager mLinearManager; | ||||
| 	private Activity mActivity; | ||||
| 	private Context mActivity; | ||||
| 	 | ||||
| 	private ServiceConnection mConnection = new ServiceConnection() { | ||||
|  | ||||
| @@ -65,7 +65,7 @@ public abstract class MissionsFragment extends Fragment | ||||
|  | ||||
| 		mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); | ||||
| 		mLinear = mPrefs.getBoolean("linear", false); | ||||
| 		 | ||||
|  | ||||
| 		// Bind the service | ||||
| 		Intent i = new Intent(); | ||||
| 		i.setClass(getActivity(), DownloadManagerService.class); | ||||
| @@ -85,14 +85,15 @@ public abstract class MissionsFragment extends Fragment | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void onAttach(Activity activity) { | ||||
| 	public void onAttach(Context activity) { | ||||
| 		super.onAttach(activity); | ||||
| 		mActivity = activity; | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
| 	public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | ||||
|  | ||||
| 	public void onDestroyView() { | ||||
| 		super.onDestroyView(); | ||||
| 		getActivity().unbindService(mConnection); | ||||
| 	} | ||||
|  | ||||
| 	@Override | ||||
|   | ||||
							
								
								
									
										4
									
								
								app/src/main/res/xml/provider_paths.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/xml/provider_paths.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <paths xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <external-path name="external_files" path="."/> | ||||
| </paths> | ||||
| @@ -0,0 +1,156 @@ | ||||
| package us.shandian.giga.get.get; | ||||
|  | ||||
| import org.junit.Ignore; | ||||
| import org.junit.Test; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.RandomAccessFile; | ||||
| import java.util.ArrayList; | ||||
|  | ||||
| import us.shandian.giga.get.DownloadDataSource; | ||||
| import us.shandian.giga.get.DownloadManagerImpl; | ||||
| import us.shandian.giga.get.DownloadMission; | ||||
|  | ||||
| import static org.junit.Assert.assertEquals; | ||||
| import static org.junit.Assert.assertNotEquals; | ||||
| import static org.junit.Assert.assertSame; | ||||
| import static org.junit.Assert.assertTrue; | ||||
| import static org.mockito.Mockito.mock; | ||||
| import static org.mockito.Mockito.never; | ||||
| import static org.mockito.Mockito.spy; | ||||
| import static org.mockito.Mockito.times; | ||||
| import static org.mockito.Mockito.verify; | ||||
| import static org.mockito.Mockito.when; | ||||
|  | ||||
| /** | ||||
|  * Test for {@link DownloadManagerImpl} | ||||
|  * | ||||
|  * TODO: test loading from .giga files, startMission and improve tests | ||||
|  */ | ||||
| public class DownloadManagerImplTest { | ||||
|  | ||||
|     private DownloadManagerImpl downloadManager; | ||||
|     private DownloadDataSource dowloadDataSource; | ||||
|     private ArrayList<DownloadMission> missions; | ||||
|  | ||||
|     @org.junit.Before | ||||
|     public void setUp() throws Exception { | ||||
|         dowloadDataSource = mock(DownloadDataSource.class); | ||||
|         missions = new ArrayList<>(); | ||||
|         for(int i = 0; i < 50; ++i){ | ||||
|             missions.add(generateFinishedDownloadMission()); | ||||
|         } | ||||
|         when(dowloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions)); | ||||
|         downloadManager = new DownloadManagerImpl(new ArrayList<String>(), dowloadDataSource); | ||||
|     } | ||||
|  | ||||
|     @Test(expected = NullPointerException.class) | ||||
|     public void testConstructorWithNullAsDownloadDataSource() { | ||||
|         new DownloadManagerImpl(new ArrayList<String>(), null); | ||||
|     } | ||||
|  | ||||
|  | ||||
|     private static DownloadMission generateFinishedDownloadMission() throws IOException { | ||||
|         File file = File.createTempFile("newpipetest", ".mp4"); | ||||
|         file.deleteOnExit(); | ||||
|         RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw"); | ||||
|         randomAccessFile.setLength(1000); | ||||
|         randomAccessFile.close(); | ||||
|         DownloadMission downloadMission = new DownloadMission(file.getName(), | ||||
|                 "http://google.com/?q=how+to+google", file.getParent()); | ||||
|         downloadMission.blocks = 1000; | ||||
|         downloadMission.done = 1000; | ||||
|         downloadMission.finished = true; | ||||
|         return spy(downloadMission); | ||||
|     } | ||||
|  | ||||
|     private static void assertMissionEquals(String message, DownloadMission expected, DownloadMission actual) { | ||||
|         if(expected == actual) return; | ||||
|         assertEquals(message + ": Name", expected.name, actual.name); | ||||
|         assertEquals(message + ": Location", expected.location, actual.location); | ||||
|         assertEquals(message + ": Url", expected.url, actual.url); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void testThatMissionsAreLoaded() throws IOException { | ||||
|         ArrayList<DownloadMission> missions = new ArrayList<>(); | ||||
|         long millis = System.currentTimeMillis(); | ||||
|         for(int i = 0; i < 50; ++i){ | ||||
|             DownloadMission  mission = generateFinishedDownloadMission(); | ||||
|             mission.timestamp = millis - i; // reverse order by timestamp | ||||
|             missions.add(mission); | ||||
|         } | ||||
|  | ||||
|         dowloadDataSource = mock(DownloadDataSource.class); | ||||
|         when(dowloadDataSource.loadMissions()).thenReturn(new ArrayList<>(missions)); | ||||
|         downloadManager = new DownloadManagerImpl(new ArrayList<String>(), dowloadDataSource); | ||||
|         verify(dowloadDataSource, times(1)).loadMissions(); | ||||
|  | ||||
|         assertEquals(50, downloadManager.getCount()); | ||||
|  | ||||
|         for(int i = 0; i < 50; ++i) { | ||||
|             assertMissionEquals("mission " + i, missions.get(50 - 1 - i), downloadManager.getMission(i)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Ignore | ||||
|     @Test | ||||
|     public void startMission() throws Exception { | ||||
|         DownloadMission mission = missions.get(0); | ||||
|         mission = spy(mission); | ||||
|         missions.set(0, mission); | ||||
|         String url = "https://github.com/favicon.ico"; | ||||
|         // create a temp file and delete it so we have a temp directory | ||||
|         File tempFile = File.createTempFile("favicon",".ico"); | ||||
|         String name = tempFile.getName(); | ||||
|         String location = tempFile.getParent(); | ||||
|         assertTrue(tempFile.delete()); | ||||
|         int id = downloadManager.startMission(url, location, name, true, 10); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void resumeMission() throws Exception { | ||||
|         DownloadMission mission = missions.get(0); | ||||
|         mission.running = true; | ||||
|         verify(mission, never()).start(); | ||||
|         downloadManager.resumeMission(0); | ||||
|         verify(mission, never()).start(); | ||||
|         mission.running = false; | ||||
|         downloadManager.resumeMission(0); | ||||
|         verify(mission, times(1)).start(); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void pauseMission() throws Exception { | ||||
|         DownloadMission mission = missions.get(0); | ||||
|         mission.running = false; | ||||
|         downloadManager.pauseMission(0); | ||||
|         verify(mission, never()).pause(); | ||||
|         mission.running = true; | ||||
|         downloadManager.pauseMission(0); | ||||
|         verify(mission, times(1)).pause(); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void deleteMission() throws Exception { | ||||
|         DownloadMission mission = missions.get(0); | ||||
|         assertEquals(mission, downloadManager.getMission(0)); | ||||
|         downloadManager.deleteMission(0); | ||||
|         verify(mission, times(1)).delete(); | ||||
|         assertNotEquals(mission, downloadManager.getMission(0)); | ||||
|         assertEquals(49, downloadManager.getCount()); | ||||
|     } | ||||
|  | ||||
|     @Test(expected = RuntimeException.class) | ||||
|     public void getMissionWithNegativeIndex() throws Exception { | ||||
|         downloadManager.getMission(-1); | ||||
|     } | ||||
|  | ||||
|     @Test | ||||
|     public void getMission() throws Exception { | ||||
|         assertSame(missions.get(0), downloadManager.getMission(0)); | ||||
|         assertSame(missions.get(1), downloadManager.getMission(1)); | ||||
|     } | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Coffeemakr
					Coffeemakr