Merge remote-tracking branch 'origin/master'

This commit is contained in:
Weblate 2017-01-26 15:46:09 +01:00
commit 0f7397e3b8
120 changed files with 1779 additions and 710 deletions

View file

@ -8,8 +8,8 @@ android {
applicationId "org.schabi.newpipe" applicationId "org.schabi.newpipe"
minSdkVersion 15 minSdkVersion 15
targetSdkVersion 25 targetSdkVersion 25
versionCode 21 versionCode 22
versionName "0.8.7" versionName "0.8.8"
} }
buildTypes { buildTypes {
release { release {
@ -46,5 +46,6 @@ dependencies {
compile 'com.google.code.gson:gson:2.4' compile 'com.google.code.gson:gson:2.4'
compile 'com.nononsenseapps:filepicker:3.0.0' compile 'com.nononsenseapps:filepicker:3.0.0'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:1.10.19'
compile 'ch.acra:acra:4.9.0' compile 'ch.acra:acra:4.9.0'
} }

View file

@ -161,6 +161,16 @@
<activity <activity
android:name=".ReCaptchaActivity" android:name=".ReCaptchaActivity"
android:label="@string/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> </application>
</manifest> </manifest>

View file

@ -48,6 +48,7 @@ public class App extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
// init crashreport // init crashreport
try { try {
final ACRAConfiguration acraConfig = new ConfigurationBuilder(this) final ACRAConfiguration acraConfig = new ConfigurationBuilder(this)

View file

@ -4,6 +4,7 @@ import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.design.widget.CollapsingToolbarLayout; import android.support.design.widget.CollapsingToolbarLayout;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
@ -31,6 +32,7 @@ import org.schabi.newpipe.info_list.InfoListAdapter;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import java.io.IOException; import java.io.IOException;
import java.util.Objects;
/** /**
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org> * Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
@ -71,6 +73,10 @@ public class ChannelActivity extends AppCompatActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme_NoActionBar);
}
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_channel); setContentView(R.layout.activity_channel);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
@ -128,7 +134,7 @@ public class ChannelActivity extends AppCompatActivity {
CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.channel_toolbar_layout); CollapsingToolbarLayout ctl = (CollapsingToolbarLayout) findViewById(R.id.channel_toolbar_layout);
ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar); ProgressBar progressBar = (ProgressBar) findViewById(R.id.progressBar);
ImageView channelBanner = (ImageView) findViewById(R.id.channel_banner_image); ImageView channelBanner = (ImageView) findViewById(R.id.channel_banner_image);
FloatingActionButton feedButton = (FloatingActionButton) findViewById(R.id.channel_rss_fab); final FloatingActionButton feedButton = (FloatingActionButton) findViewById(R.id.channel_rss_fab);
ImageView avatarView = (ImageView) findViewById(R.id.channel_avatar_view); ImageView avatarView = (ImageView) findViewById(R.id.channel_avatar_view);
ImageView haloView = (ImageView) findViewById(R.id.channel_avatar_halo); ImageView haloView = (ImageView) findViewById(R.id.channel_avatar_halo);

View file

@ -4,13 +4,13 @@ import android.content.Intent;
import android.media.AudioManager; import android.media.AudioManager;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.PermissionHelper;
/** /**
* Created by Christian Schabesberger on 02.08.16. * Created by Christian Schabesberger on 02.08.16.
@ -32,7 +32,7 @@ import org.schabi.newpipe.settings.SettingsActivity;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
public class MainActivity extends AppCompatActivity { public class MainActivity extends ThemableActivity {
private Fragment mainFragment = null; private Fragment mainFragment = null;
@ -40,7 +40,6 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); setContentView(R.layout.activity_main);
setVolumeControlStream(AudioManager.STREAM_MUSIC); setVolumeControlStream(AudioManager.STREAM_MUSIC);
mainFragment = getSupportFragmentManager() mainFragment = getSupportFragmentManager()
.findFragmentById(R.id.search_fragment); .findFragmentById(R.id.search_fragment);
@ -52,8 +51,6 @@ public class MainActivity extends AppCompatActivity {
MenuInflater inflater = getMenuInflater(); MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.main_menu, menu); inflater.inflate(R.menu.main_menu, menu);
mainFragment.onCreateOptionsMenu(menu, inflater);
return true; return true;
} }
@ -74,13 +71,15 @@ public class MainActivity extends AppCompatActivity {
return true; return true;
} }
case R.id.action_show_downloads: { case R.id.action_show_downloads: {
if(!PermissionHelper.checkStoragePermissions(this)) {
return false;
}
Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class); Intent intent = new Intent(this, org.schabi.newpipe.download.DownloadActivity.class);
startActivity(intent); startActivity(intent);
return true; return true;
} }
default: default:
return mainFragment.onOptionsItemSelected(item) || return super.onOptionsItemSelected(item);
super.onOptionsItemSelected(item);
} }
} }
} }

View file

@ -0,0 +1,21 @@
package org.schabi.newpipe;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import java.util.Objects;
import static org.schabi.newpipe.R.attr.theme;
public class ThemableActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme);
}
}
}

View file

@ -5,7 +5,6 @@ import android.media.AudioManager;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v7.app.AppCompatActivity;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -14,6 +13,7 @@ import android.widget.Toast;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService;
@ -39,7 +39,7 @@ import java.util.HashSet;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
public class VideoItemDetailActivity extends AppCompatActivity { public class VideoItemDetailActivity extends ThemableActivity {
/** /**
* Removes invisible separators (\p{Z}) and punctuation characters including * Removes invisible separators (\p{Z}) and punctuation characters including
@ -164,19 +164,10 @@ public class VideoItemDetailActivity extends AppCompatActivity {
NavUtils.navigateUpTo(this, intent); NavUtils.navigateUpTo(this, intent);
return true; return true;
} else { } else {
return fragment.onOptionsItemSelected(item) || return super.onOptionsItemSelected(item);
super.onOptionsItemSelected(item);
} }
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
fragment.onCreateOptionsMenu(menu, getMenuInflater());
return true;
}
/** /**
* Retrieves all Strings which look remotely like URLs from a text. * Retrieves all Strings which look remotely like URLs from a text.
* Used if NewPipe was called through share menu. * Used if NewPipe was called through share menu.

View file

@ -21,6 +21,7 @@ import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -30,7 +31,6 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.view.MenuItem;
import android.widget.Toast; import android.widget.Toast;
import com.google.android.exoplayer.util.Util; import com.google.android.exoplayer.util.Util;
@ -39,26 +39,26 @@ import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason; import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import java.util.Vector;
import org.schabi.newpipe.ActivityCommunicator; import org.schabi.newpipe.ActivityCommunicator;
import org.schabi.newpipe.ChannelActivity; import org.schabi.newpipe.ChannelActivity;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.ImageErrorLoadingListener; import org.schabi.newpipe.ImageErrorLoadingListener;
import org.schabi.newpipe.Localization; import org.schabi.newpipe.Localization;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ReCaptchaActivity;
import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.MediaFormat; import org.schabi.newpipe.extractor.MediaFormat;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.stream_info.AudioStream;
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
import org.schabi.newpipe.extractor.stream_info.StreamPreviewInfo;
import org.schabi.newpipe.extractor.stream_info.VideoStream; import org.schabi.newpipe.extractor.stream_info.VideoStream;
import org.schabi.newpipe.info_list.InfoItemBuilder;
import org.schabi.newpipe.player.BackgroundPlayer; import org.schabi.newpipe.player.BackgroundPlayer;
import org.schabi.newpipe.player.PlayVideoActivity;
import org.schabi.newpipe.player.ExoPlayerActivity; import org.schabi.newpipe.player.ExoPlayerActivity;
import org.schabi.newpipe.player.PlayVideoActivity;
import org.schabi.newpipe.report.ErrorActivity;
import java.util.Vector;
import org.schabi.newpipe.util.PermissionHelper;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST; import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
@ -393,6 +393,10 @@ public class VideoItemDetailFragment extends Fragment {
actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() { actionBarHandler.setOnDownloadListener(new ActionBarHandler.OnActionListener() {
@Override @Override
public void onActionSelected(int selectedStreamId) { public void onActionSelected(int selectedStreamId) {
if(!PermissionHelper.checkStoragePermissions(getActivity())) {
return;
}
try { try {
Bundle args = new Bundle(); Bundle args = new Bundle();
@ -629,6 +633,7 @@ public class VideoItemDetailFragment extends Fragment {
onNotSpecifiedContentError(); onNotSpecifiedContentError();
} }
}); });
setHasOptionsMenu(true);
} }
@Override @Override
@ -650,7 +655,6 @@ public class VideoItemDetailFragment extends Fragment {
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
Activity a = getActivity(); Activity a = getActivity();
infoItemBuilder = new InfoItemBuilder(a, a.findViewById(android.R.id.content)); infoItemBuilder = new InfoItemBuilder(a, a.findViewById(android.R.id.content));
if (android.os.Build.VERSION.SDK_INT < 18) { if (android.os.Build.VERSION.SDK_INT < 18) {

View file

@ -13,8 +13,8 @@ import android.os.IBinder;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -28,8 +28,10 @@ import android.widget.SeekBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.report.ErrorActivity; import org.schabi.newpipe.report.ErrorActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.settings.SettingsActivity;
import java.io.File; import java.io.File;
@ -42,7 +44,7 @@ import us.shandian.giga.ui.fragment.MissionsFragment;
import us.shandian.giga.util.CrashHandler; import us.shandian.giga.util.CrashHandler;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;
public class DownloadActivity extends AppCompatActivity implements AdapterView.OnItemClickListener{ public class DownloadActivity extends ThemableActivity implements AdapterView.OnItemClickListener{
public static final String INTENT_DOWNLOAD = "us.shandian.giga.intent.DOWNLOAD"; public static final String INTENT_DOWNLOAD = "us.shandian.giga.intent.DOWNLOAD";
@ -53,26 +55,11 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
private MissionsFragment mFragment; private MissionsFragment mFragment;
private DownloadManager mManager;
private DownloadManagerService.DMBinder mBinder;
private String mPendingUrl; private String mPendingUrl;
private SharedPreferences mPrefs; 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 @Override
@TargetApi(21) @TargetApi(21)
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -83,7 +70,6 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
Intent i = new Intent(); Intent i = new Intent();
i.setClass(this, DownloadManagerService.class); i.setClass(this, DownloadManagerService.class);
startService(i); startService(i);
bindService(i, mConnection, Context.BIND_AUTO_CREATE);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_downloader); setContentView(R.layout.activity_downloader);
@ -91,7 +77,7 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
//noinspection ConstantConditions //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 actionBar = getSupportActionBar();
actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.downloads_title); actionBar.setTitle(R.string.downloads_title);
@ -202,22 +188,24 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
@Override @Override
public boolean onMenuItemClick(MenuItem item) { public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.okay) { 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(); String fName = name.getText().toString().trim();
File f = new File(mManager.getLocation() + "/" + fName); File f = new File(location, fName);
if (f.exists()) { if (f.exists()) {
Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show(); Toast.makeText(DownloadActivity.this, R.string.msg_exists, Toast.LENGTH_SHORT).show();
} else { } else {
DownloadManagerService.startMission(
while (mBinder == null); DownloadActivity.this,
getIntent().getData().toString(), location, fName,
int res = mManager.startMission( audioButton.isChecked(), threads.getProgress() + 1);
getIntent().getData().toString(),
fName,
audioButton.isChecked(),
threads.getProgress() + 1);
mBinder.onMissionAdded(mManager.getMission(res));
mFragment.notifyChange(); mFragment.notifyChange();
mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit(); mPrefs.edit().putInt(THREADS, threads.getProgress() + 1).commit();
@ -273,8 +261,8 @@ public class DownloadActivity extends AppCompatActivity implements AdapterView.O
return true; return true;
} }
default: default:
return mFragment.onOptionsItemSelected(item) || return super.onOptionsItemSelected(item);
super.onOptionsItemSelected(item);
} }
} }
} }

View file

@ -26,6 +26,8 @@ import android.widget.TextView;
import org.schabi.newpipe.App; import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.settings.NewPipeSettings;
import java.io.File; import java.io.File;
import java.util.ArrayList; 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 AUDIO_URL = "audio_url";
public static final String VIDEO_URL = "video_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() { public DownloadDialog() {
} }
@ -102,12 +86,6 @@ public class DownloadDialog extends DialogFragment {
if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) if(ContextCompat.checkSelfPermission(this.getContext(),Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED)
ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},0); 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); return inflater.inflate(R.layout.dialog_url, container);
} }
@ -219,36 +197,22 @@ public class DownloadDialog extends DialogFragment {
String fName = name.getText().toString().trim(); String fName = name.getText().toString().trim();
// todo: add timeout? would be bad if the thread gets locked dueto this. boolean isAudio = audioButton.isChecked();
while (mBinder == null); String url, location, filename;
if(isAudio) {
if(audioButton.isChecked()){ url = arguments.getString(AUDIO_URL);
int res = mManager.startMission( location = NewPipeSettings.getAudioDownloadPath(getContext());
arguments.getString(AUDIO_URL), filename = fName + arguments.getString(FILE_SUFFIX_AUDIO);
fName + arguments.getString(FILE_SUFFIX_AUDIO), } else {
audioButton.isChecked(), url = arguments.getString(VIDEO_URL);
threads.getProgress() + 1); location = NewPipeSettings.getVideoDownloadPath(getContext());
DownloadMission mission = mManager.getMission(res); filename = fName + arguments.getString(FILE_SUFFIX_VIDEO);
mBinder.onMissionAdded(mission);
// add download listener to allow media scan notification
DownloadListener listener = new DownloadListener(getContext(), mission);
mission.addListener(listener);
} }
if(videoButton.isChecked()){ DownloadManagerService.startMission(getContext(), url, location, filename, isAudio,
int res = mManager.startMission( threads.getProgress() + 1);
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);
}
getDialog().dismiss(); getDialog().dismiss();
} }
private void download(String url, String title, private void download(String url, String title,

View file

@ -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 ...
}
}

View file

@ -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 class FileDownloader extends AsyncTask<Void, Integer, Void> {
public static final String TAG = "FileDownloader"; public static final String TAG = "FileDownloader";

View file

@ -4,7 +4,6 @@ import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import java.io.IOException; import java.io.IOException;
import java.util.List;
/** /**
* Created by Christian Schabesberger on 10.08.15. * Created by Christian Schabesberger on 10.08.15.
@ -26,7 +25,6 @@ import java.util.List;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
@SuppressWarnings("ALL")
public abstract class SearchEngine { public abstract class SearchEngine {
public static class NothingFoundException extends ExtractionException { public static class NothingFoundException extends ExtractionException {
public NothingFoundException(String message) { public NothingFoundException(String message) {

View file

@ -85,18 +85,18 @@ public class YoutubeSearchEngine extends SearchEngine {
Element el; Element el;
// both types of spell correction item // both types of spell correction item
if (!((el = item.select("div[class*=\"spell-correction\"]").first()) == null)) { if ((el = item.select("div[class*=\"spell-correction\"]").first()) != null) {
collector.setSuggestion(el.select("a").first().text()); collector.setSuggestion(el.select("a").first().text());
if(list.children().size() == 1) { if(list.children().size() == 1) {
throw new NothingFoundException("Did you mean: " + el.select("a").first().text()); throw new NothingFoundException("Did you mean: " + el.select("a").first().text());
} }
// search message item // search message item
} else if (!((el = item.select("div[class*=\"search-message\"]").first()) == null)) { } else if ((el = item.select("div[class*=\"search-message\"]").first()) != null) {
//result.errorMessage = el.text(); //result.errorMessage = el.text();
throw new NothingFoundException(el.text()); throw new NothingFoundException(el.text());
// video item type // video item type
} else if (!((el = item.select("div[class*=\"yt-lockup-video\"").first()) == null)) { } else if ((el = item.select("div[class*=\"yt-lockup-video\"").first()) != null) {
collector.commit(extractPreviewInfo(el)); collector.commit(extractPreviewInfo(el));
} else { } else {
//noinspection ConstantConditions //noinspection ConstantConditions

View file

@ -46,7 +46,7 @@ public class YoutubeService extends StreamingService {
@Override @Override
public StreamExtractor getExtractorInstance(String url) public StreamExtractor getExtractorInstance(String url)
throws ExtractionException, IOException { throws ExtractionException, IOException {
UrlIdHandler urlIdHandler = new YoutubeStreamUrlIdHandler(); UrlIdHandler urlIdHandler = YoutubeStreamUrlIdHandler.getInstance();
if(urlIdHandler.acceptUrl(url)) { if(urlIdHandler.acceptUrl(url)) {
return new YoutubeStreamExtractor(urlIdHandler, url, getServiceId()); return new YoutubeStreamExtractor(urlIdHandler, url, getServiceId());
} }
@ -61,7 +61,7 @@ public class YoutubeService extends StreamingService {
@Override @Override
public UrlIdHandler getUrlIdHandlerInstance() { public UrlIdHandler getUrlIdHandlerInstance() {
return new YoutubeStreamUrlIdHandler(); return YoutubeStreamUrlIdHandler.getInstance();
} }
@Override @Override

View file

@ -88,7 +88,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// $$el_type$$ will be replaced by the actual el_type (se the declarations below) // $$el_type$$ will be replaced by the actual el_type (se the declarations below)
private static final String GET_VIDEO_INFO_URL = 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"; "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"; private static final String EL_INFO = "el=info";
public enum ItagType { public enum ItagType {
@ -184,7 +184,7 @@ public class YoutubeStreamExtractor extends StreamExtractor {
// cached values // cached values
private static volatile String decryptionCode = ""; private static volatile String decryptionCode = "";
UrlIdHandler urlidhandler = new YoutubeStreamUrlIdHandler(); UrlIdHandler urlidhandler = YoutubeStreamUrlIdHandler.getInstance();
String pageUrl = ""; String pageUrl = "";
public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl, int serviceId) public YoutubeStreamExtractor(UrlIdHandler urlIdHandler, String pageUrl, int serviceId)

View file

@ -1,12 +1,21 @@
package org.schabi.newpipe.extractor.services.youtube; package org.schabi.newpipe.extractor.services.youtube;
import android.support.annotation.NonNull;
import org.schabi.newpipe.extractor.Downloader;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.Parser; import org.schabi.newpipe.extractor.Parser;
import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException; import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.util.regex.Matcher;
/** /**
* Created by Christian Schabesberger on 02.02.16. * Created by Christian Schabesberger on 02.02.16.
@ -29,45 +38,55 @@ import java.net.URLDecoder;
*/ */
public class YoutubeStreamUrlIdHandler implements UrlIdHandler { public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
@SuppressWarnings("WeakerAccess")
private static final YoutubeStreamUrlIdHandler instance = new YoutubeStreamUrlIdHandler();
private static final String ID_PATTERN = "([\\-a-zA-Z0-9_]{11})";
private YoutubeStreamUrlIdHandler() {}
public static YoutubeStreamUrlIdHandler getInstance() {
return instance;
}
@Override @Override
public String getUrl(String videoId) { public String getUrl(String videoId) {
return "https://www.youtube.com/watch?v=" + videoId; return "https://www.youtube.com/watch?v=" + videoId;
} }
@SuppressWarnings("WeakerAccess")
@Override @Override
public String getId(String url) throws ParsingException, IllegalArgumentException { public String getId(String url) throws ParsingException, IllegalArgumentException {
if(url.isEmpty()) if(url.isEmpty()) {
{
throw new IllegalArgumentException("The url parameter should not be empty"); throw new IllegalArgumentException("The url parameter should not be empty");
} }
String id;
if(url.contains("youtube")) { String id;
if(url.contains("attribution_link")) { String lowercaseUrl = url.toLowerCase();
if(lowercaseUrl.contains("youtube")) {
if (url.contains("attribution_link")) {
try { try {
String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url); String escapedQuery = Parser.matchGroup1("u=(.[^&|$]*)", url);
String query = URLDecoder.decode(escapedQuery, "UTF-8"); String query = URLDecoder.decode(escapedQuery, "UTF-8");
id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", query); id = Parser.matchGroup1("v=" + ID_PATTERN, query);
} catch(UnsupportedEncodingException uee) { } catch (UnsupportedEncodingException uee) {
throw new ParsingException("Could not parse attribution_link", uee); throw new ParsingException("Could not parse attribution_link", uee);
} }
} } else if(lowercaseUrl.contains("youtube.com/shared?ci=")) {
else if(url.contains("vnd.youtube")) return getRealIdFromSharedLink(url);
{ } else if (url.contains("vnd.youtube")) {
id = Parser.matchGroup1("([\\-a-zA-Z0-9_]{11}).*", url); id = Parser.matchGroup1(ID_PATTERN, url);
} else if (url.contains("embed")) {
id = Parser.matchGroup1("embed/" + ID_PATTERN, url);
} else if(url.contains("googleads")) { } else if(url.contains("googleads")) {
throw new FoundAdException("Error found add: " + url); throw new FoundAdException("Error found add: " + url);
} else { } else {
id = Parser.matchGroup1("[?&]v=([\\-a-zA-Z0-9_]{11})", url); id = Parser.matchGroup1("[?&]v=" + ID_PATTERN, url);
} }
} }
else if(url.contains("youtu.be")) { else if(lowercaseUrl.contains("youtu.be")) {
if(url.contains("v=")) { if(url.contains("v=")) {
id = Parser.matchGroup1("v=([\\-a-zA-Z0-9_]{11})", url); id = Parser.matchGroup1("v=" + ID_PATTERN, url);
} else { } else {
id = Parser.matchGroup1("youtu\\.be/([a-zA-Z0-9_-]{11})", url); id = Parser.matchGroup1("[Yy][Oo][Uu][Tt][Uu]\\.[Bb][Ee]/" + ID_PATTERN, url);
} }
} }
else { else {
@ -82,12 +101,55 @@ public class YoutubeStreamUrlIdHandler implements UrlIdHandler {
} }
} }
/**
* Get the real url from a shared uri.
*
* Shared URI's look like this:
* <pre>
* * https://www.youtube.com/shared?ci=PJICrTByb3E
* * vnd.youtube://www.youtube.com/shared?ci=PJICrTByb3E&feature=twitter-deep-link
* </pre>
* @param url The shared url
* @return the id of the stream
* @throws ParsingException
*/
private @NonNull String getRealIdFromSharedLink(String url) throws ParsingException {
URI uri;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
throw new ParsingException("Invalid shared link", e);
}
String sharedId = getSharedId(uri);
Downloader downloader = NewPipe.getDownloader();
String content;
try {
content = downloader.download("https://www.youtube.com/shared?ci=" + sharedId);
} catch (IOException | ReCaptchaException e) {
throw new ParsingException("Unable to resolve shared link", e);
}
// is this bad? is this fragile?:
String realId = Parser.matchGroup1("rel=\"shortlink\" href=\"https://youtu.be/" + ID_PATTERN, content);
if(sharedId.equals(realId)) {
throw new ParsingException("Got same id for as shared id: " + sharedId);
}
return realId;
}
private @NonNull String getSharedId(URI uri) throws ParsingException {
if (!"/shared".equals(uri.getPath())) {
throw new ParsingException("Not a shared link: " + uri.toString() + " (path != " + uri.getPath() + ")");
}
return Parser.matchGroup1("ci=" + ID_PATTERN, uri.getQuery());
}
public String cleanUrl(String complexUrl) throws ParsingException { public String cleanUrl(String complexUrl) throws ParsingException {
return getUrl(getId(complexUrl)); return getUrl(getId(complexUrl));
} }
@Override @Override
public boolean acceptUrl(String videoUrl) { public boolean acceptUrl(String videoUrl) {
videoUrl = videoUrl.toLowerCase();
return videoUrl.contains("youtube") || return videoUrl.contains("youtube") ||
videoUrl.contains("youtu.be"); videoUrl.contains("youtu.be");
} }

View file

@ -1,5 +1,6 @@
package org.schabi.newpipe.extractor.stream_info; package org.schabi.newpipe.extractor.stream_info;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.UrlIdHandler; import org.schabi.newpipe.extractor.UrlIdHandler;
import org.schabi.newpipe.extractor.exceptions.FoundAdException; import org.schabi.newpipe.extractor.exceptions.FoundAdException;
import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.exceptions.ParsingException;
@ -60,7 +61,7 @@ public class StreamPreviewInfoCollector {
if (urlIdHandler == null) { if (urlIdHandler == null) {
throw new ParsingException("Error: UrlIdHandler not set"); throw new ParsingException("Error: UrlIdHandler not set");
} else if(!resultItem.webpage_url.isEmpty()) { } else if(!resultItem.webpage_url.isEmpty()) {
resultItem.id = (new YoutubeStreamUrlIdHandler()).getId(resultItem.webpage_url); resultItem.id = NewPipe.getService(serviceId).getUrlIdHandlerInstance().getId(resultItem.webpage_url);
} }
resultItem.title = extractor.getTitle(); resultItem.title = extractor.getTitle();
resultItem.stream_type = extractor.getStreamType(); resultItem.stream_type = extractor.getStreamType();

View file

@ -30,13 +30,13 @@ import org.schabi.newpipe.R;
public class InfoItemHolder extends RecyclerView.ViewHolder { public class InfoItemHolder extends RecyclerView.ViewHolder {
public ImageView itemThumbnailView; public final ImageView itemThumbnailView;
public TextView itemVideoTitleView, public final TextView itemVideoTitleView,
itemUploaderView, itemUploaderView,
itemDurationView, itemDurationView,
itemUploadDateView, itemUploadDateView,
itemViewCountView; itemViewCountView;
public Button itemButton; public final Button itemButton;
public InfoItemHolder(View v) { public InfoItemHolder(View v) {
super(v); super(v);
@ -48,4 +48,5 @@ public class InfoItemHolder extends RecyclerView.ViewHolder {
itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView); itemViewCountView = (TextView) v.findViewById(R.id.itemViewCountView);
itemButton = (Button) v.findViewById(R.id.item_button); itemButton = (Button) v.findViewById(R.id.item_button);
} }
} }

View file

@ -34,11 +34,12 @@ import java.util.Vector;
public class InfoListAdapter extends RecyclerView.Adapter<InfoItemHolder> { public class InfoListAdapter extends RecyclerView.Adapter<InfoItemHolder> {
InfoItemBuilder infoItemBuilder = null; private final InfoItemBuilder infoItemBuilder;
List<StreamPreviewInfo> streamList = new Vector<>(); private final List<StreamPreviewInfo> streamList;
public InfoListAdapter(Activity a, View rootView) { public InfoListAdapter(Activity a, View rootView) {
infoItemBuilder = new InfoItemBuilder(a, rootView); infoItemBuilder = new InfoItemBuilder(a, rootView);
streamList = new Vector<>();
} }
public void setOnItemSelectedListener public void setOnItemSelectedListener
@ -54,7 +55,7 @@ public class InfoListAdapter extends RecyclerView.Adapter<InfoItemHolder> {
} }
public void clearSteamItemList() { public void clearSteamItemList() {
streamList = new Vector<>(); streamList.clear();
notifyDataSetChanged(); notifyDataSetChanged();
} }

View file

@ -14,7 +14,6 @@ import android.preference.PreferenceManager;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.Log; import android.util.Log;
@ -35,6 +34,7 @@ import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.Downloader; import org.schabi.newpipe.Downloader;
import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.ThemableActivity;
import org.schabi.newpipe.extractor.Parser; import org.schabi.newpipe.extractor.Parser;
import java.io.PrintWriter; import java.io.PrintWriter;
@ -65,7 +65,7 @@ import java.util.Vector;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
public class ErrorActivity extends AppCompatActivity { public class ErrorActivity extends ThemableActivity {
public static class ErrorInfo implements Parcelable { public static class ErrorInfo implements Parcelable {
public int userAction; public int userAction;
public String request; public String request;

View file

@ -56,13 +56,15 @@ public class SearchInfoItemFragment extends Fragment {
private static final String TAG = SearchInfoItemFragment.class.toString(); private static final String TAG = SearchInfoItemFragment.class.toString();
/**
* Listener for search queries
*/
public class SearchQueryListener implements SearchView.OnQueryTextListener { public class SearchQueryListener implements SearchView.OnQueryTextListener {
@Override @Override
public boolean onQueryTextSubmit(String query) { public boolean onQueryTextSubmit(String query) {
Activity a = getActivity(); Activity a = getActivity();
try { try {
searchQuery = query;
search(query); search(query);
// hide virtual keyboard // hide virtual keyboard
@ -89,8 +91,6 @@ public class SearchInfoItemFragment extends Fragment {
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
View bg = a.findViewById(R.id.mainBG);
bg.setVisibility(View.GONE);
return true; return true;
} }
@ -108,12 +108,10 @@ public class SearchInfoItemFragment extends Fragment {
private boolean isLoading = false; private boolean isLoading = false;
private ProgressBar loadingIndicator = null; private ProgressBar loadingIndicator = null;
private SearchView searchView = null;
private int pageNumber = 0; private int pageNumber = 0;
private SuggestionListAdapter suggestionListAdapter = null; private SuggestionListAdapter suggestionListAdapter = null;
private InfoListAdapter infoListAdapter = null; private InfoListAdapter infoListAdapter = null;
private LinearLayoutManager streamInfoListLayoutManager = null; private LinearLayoutManager streamInfoListLayoutManager = null;
private RecyclerView recyclerView = null;
// savedInstanceBundle arguments // savedInstanceBundle arguments
private static final String QUERY = "query"; private static final String QUERY = "query";
@ -126,23 +124,32 @@ public class SearchInfoItemFragment extends Fragment {
public SearchInfoItemFragment() { public SearchInfoItemFragment() {
} }
// TODO: Customize parameter initialization
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static SearchInfoItemFragment newInstance(int columnCount) { public static SearchInfoItemFragment newInstance(int streamingServiceId, String searchQuery) {
Bundle args = new Bundle();
args.putInt(STREAMING_SERVICE, streamingServiceId);
args.putString(QUERY, searchQuery);
SearchInfoItemFragment fragment = new SearchInfoItemFragment(); SearchInfoItemFragment fragment = new SearchInfoItemFragment();
fragment.setArguments(args);
return fragment; return fragment;
} }
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
searchQuery = "";
if (savedInstanceState != null) { if (savedInstanceState != null) {
searchQuery = savedInstanceState.getString(QUERY); searchQuery = savedInstanceState.getString(QUERY);
streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE); streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
} else { } else {
try { try {
streamingServiceId = NewPipe.getIdOfService("Youtube"); Bundle args = getArguments();
if(args != null) {
searchQuery = args.getString(QUERY);
streamingServiceId = args.getInt(STREAMING_SERVICE);
} else {
streamingServiceId = NewPipe.getIdOfService("Youtube");
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
ErrorActivity.reportError(getActivity(), e, null, ErrorActivity.reportError(getActivity(), e, null,
@ -153,13 +160,14 @@ public class SearchInfoItemFragment extends Fragment {
} }
} }
setHasOptionsMenu(true);
SearchWorker sw = SearchWorker.getInstance(); SearchWorker sw = SearchWorker.getInstance();
sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() { sw.setSearchWorkerResultListener(new SearchWorker.SearchWorkerResultListener() {
@Override @Override
public void onResult(SearchResult result) { public void onResult(SearchResult result) {
infoListAdapter.addStreamItemList(result.resultList); infoListAdapter.addStreamItemList(result.resultList);
isLoading = false; setDoneLoading();
loadingIndicator.setVisibility(View.GONE);
} }
@Override @Override
@ -167,8 +175,7 @@ public class SearchInfoItemFragment extends Fragment {
//setListShown(true); //setListShown(true);
Toast.makeText(getActivity(), getString(stringResource), Toast.makeText(getActivity(), getString(stringResource),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
isLoading = false; setDoneLoading();
loadingIndicator.setVisibility(View.GONE);
} }
@Override @Override
@ -176,8 +183,7 @@ public class SearchInfoItemFragment extends Fragment {
//setListShown(true); //setListShown(true);
Toast.makeText(getActivity(), message, Toast.makeText(getActivity(), message,
Toast.LENGTH_LONG).show(); Toast.LENGTH_LONG).show();
isLoading = false; setDoneLoading();
loadingIndicator.setVisibility(View.GONE);
} }
@Override @Override
@ -191,6 +197,7 @@ public class SearchInfoItemFragment extends Fragment {
RECAPTCHA_REQUEST); RECAPTCHA_REQUEST);
} }
}); });
} }
@Override @Override
@ -200,7 +207,7 @@ public class SearchInfoItemFragment extends Fragment {
Context context = view.getContext(); Context context = view.getContext();
loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar); loadingIndicator = (ProgressBar) view.findViewById(R.id.progressBar);
recyclerView = (RecyclerView) view.findViewById(R.id.list); RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
streamInfoListLayoutManager = new LinearLayoutManager(context); streamInfoListLayoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(streamInfoListLayoutManager); recyclerView.setLayoutManager(streamInfoListLayoutManager);
@ -209,15 +216,12 @@ public class SearchInfoItemFragment extends Fragment {
infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() { infoListAdapter.setOnItemSelectedListener(new InfoItemBuilder.OnItemSelectedListener() {
@Override @Override
public void selected(String url) { public void selected(String url) {
Intent i = new Intent(getActivity(), VideoItemDetailActivity.class); startDetailActivity(url);
i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
getActivity().startActivity(i);
} }
}); });
recyclerView.setAdapter(infoListAdapter); recyclerView.setAdapter(infoListAdapter);
recyclerView.clearOnScrollListeners();
recyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
int pastVisiblesItems, visibleItemCount, totalItemCount; int pastVisiblesItems, visibleItemCount, totalItemCount;
@ -239,14 +243,26 @@ public class SearchInfoItemFragment extends Fragment {
return view; return view;
} }
@Override private void startDetailActivity(String url) {
public void onAttach(Context context) { Intent i = new Intent(getActivity(), VideoItemDetailActivity.class);
super.onAttach(context); i.putExtra(VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
i.putExtra(VideoItemDetailFragment.VIDEO_URL, url);
getActivity().startActivity(i);
} }
@Override @Override
public void onDetach() { public void onStart() {
super.onDetach(); super.onStart();
if(!searchQuery.isEmpty()) {
search(searchQuery);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(QUERY, searchQuery);
outState.putInt(STREAMING_SERVICE, streamingServiceId);
} }
@Override @Override
@ -255,15 +271,10 @@ public class SearchInfoItemFragment extends Fragment {
inflater.inflate(R.menu.search_menu, menu); inflater.inflate(R.menu.search_menu, menu);
MenuItem searchItem = menu.findItem(R.id.action_search); MenuItem searchItem = menu.findItem(R.id.action_search);
searchView = (SearchView) searchItem.getActionView(); SearchView searchView = (SearchView) searchItem.getActionView();
setupSearchView(searchView); setupSearchView(searchView);
} }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return super.onOptionsItemSelected(item);
}
private void setupSearchView(SearchView searchView) { private void setupSearchView(SearchView searchView) {
suggestionListAdapter = new SuggestionListAdapter(getActivity()); suggestionListAdapter = new SuggestionListAdapter(getActivity());
searchView.setSuggestionsAdapter(suggestionListAdapter); searchView.setSuggestionsAdapter(suggestionListAdapter);
@ -278,7 +289,9 @@ public class SearchInfoItemFragment extends Fragment {
private void search(String query) { private void search(String query) {
infoListAdapter.clearSteamItemList(); infoListAdapter.clearSteamItemList();
pageNumber = 0; pageNumber = 0;
searchQuery = query;
search(query, pageNumber); search(query, pageNumber);
hideBackground();
loadingIndicator.setVisibility(View.VISIBLE); loadingIndicator.setVisibility(View.VISIBLE);
} }
@ -288,6 +301,20 @@ public class SearchInfoItemFragment extends Fragment {
sw.search(streamingServiceId, query, page, getActivity()); sw.search(streamingServiceId, query, page, getActivity());
} }
private void setDoneLoading() {
this.isLoading = false;
loadingIndicator.setVisibility(View.GONE);
}
/**
* Hides the "dummy" background when no results are shown
*/
private void hideBackground() {
View view = getView();
if(view == null) return;
view.findViewById(R.id.mainBG).setVisibility(View.GONE);
}
private void searchSuggestions(String query) { private void searchSuggestions(String query) {
SuggestionSearchRunnable suggestionSearchRunnable = SuggestionSearchRunnable suggestionSearchRunnable =
new SuggestionSearchRunnable(streamingServiceId, query, getActivity(), suggestionListAdapter); new SuggestionSearchRunnable(streamingServiceId, query, getActivity(), suggestionListAdapter);

View file

@ -25,8 +25,8 @@ import android.support.v7.widget.SearchView;
public class SearchSuggestionListener implements SearchView.OnSuggestionListener{ public class SearchSuggestionListener implements SearchView.OnSuggestionListener{
private SearchView searchView; private final SearchView searchView;
private SuggestionListAdapter adapter; private final SuggestionListAdapter adapter;
public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) { public SearchSuggestionListener(SearchView searchView, SuggestionListAdapter adapter) {
this.searchView = searchView; this.searchView = searchView;

View file

@ -82,6 +82,7 @@ public class SearchWorker {
} }
@Override @Override
public void run() { public void run() {
final String serviceName = NewPipe.getNameOfService(serviceId);
SearchResult result = null; SearchResult result = null;
SearchEngine engine = null; SearchEngine engine = null;
@ -119,7 +120,7 @@ public class SearchWorker {
View rootView = a.findViewById(android.R.id.content); View rootView = a.findViewById(android.R.id.content);
ErrorActivity.reportError(h, a, result.errors, null, rootView, ErrorActivity.reportError(h, a, result.errors, null, rootView,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
/* todo: this shoudl not be assigned static */ YOUTUBE, query, R.string.light_parsing_error)); serviceName, query, R.string.light_parsing_error));
} }
// hard errors: // hard errors:
@ -148,8 +149,7 @@ public class SearchWorker {
} catch(ExtractionException e) { } catch(ExtractionException e) {
ErrorActivity.reportError(h, a, e, null, null, ErrorActivity.reportError(h, a, e, null, null,
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,
/* todo: this shoudl not be assigned static */ serviceName, query, R.string.parsing_error));
YOUTUBE, query, R.string.parsing_error));
//postNewErrorToast(h, R.string.parsing_error); //postNewErrorToast(h, R.string.parsing_error);
e.printStackTrace(); e.printStackTrace();

View file

@ -3,10 +3,8 @@ package org.schabi.newpipe.search_fragment;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.support.v4.widget.CursorAdapter; import android.support.v4.widget.ResourceCursorAdapter;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import java.util.List; import java.util.List;
@ -31,52 +29,56 @@ import java.util.List;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>. * along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/ */
public class SuggestionListAdapter extends CursorAdapter { /**
* {@link ResourceCursorAdapter} to display suggestions.
*/
public class SuggestionListAdapter extends ResourceCursorAdapter {
private static final String[] columns = new String[]{"_id", "title"};
private static final int INDEX_ID = 0;
private static final int INDEX_TITLE = 1;
private String[] columns = new String[]{"_id", "title"};
public SuggestionListAdapter(Context context) { public SuggestionListAdapter(Context context) {
super(context, null, false); super(context, android.R.layout.simple_list_item_1, null, 0);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
ViewHolder viewHolder;
View view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false);
viewHolder = new ViewHolder();
viewHolder.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
view.setTag(viewHolder);
return view;
} }
@Override @Override
public void bindView(View view, Context context, Cursor cursor) { public void bindView(View view, Context context, Cursor cursor) {
ViewHolder viewHolder = (ViewHolder) view.getTag(); ViewHolder viewHolder = new ViewHolder(view);
viewHolder.suggestionTitle.setText(cursor.getString(1)); viewHolder.suggestionTitle.setText(cursor.getString(INDEX_TITLE));
} }
/**
* Update the suggestion list
* @param suggestions the list of suggestions
*/
public void updateAdapter(List<String> suggestions) { public void updateAdapter(List<String> suggestions) {
MatrixCursor cursor = new MatrixCursor(columns); MatrixCursor cursor = new MatrixCursor(columns, suggestions.size());
int i = 0; int i = 0;
for (String s : suggestions) { for (String suggestion : suggestions) {
String[] temp = new String[2]; String[] columnValues = new String[columns.length];
temp[0] = Integer.toString(i); columnValues[INDEX_TITLE] = suggestion;
temp[1] = s; columnValues[INDEX_ID] = Integer.toString(i);
cursor.addRow(columnValues);
i++; i++;
cursor.addRow(temp);
} }
changeCursor(cursor); changeCursor(cursor);
} }
/**
* Get the suggestion for a position
* @param position the position of the suggestion
* @return the suggestion
*/
public String getSuggestion(int position) { public String getSuggestion(int position) {
return ((Cursor) getItem(position)).getString(1); return ((Cursor) getItem(position)).getString(INDEX_TITLE);
} }
private class ViewHolder { private class ViewHolder {
public TextView suggestionTitle; private final TextView suggestionTitle;
private ViewHolder(View view) {
this.suggestionTitle = (TextView) view.findViewById(android.R.id.text1);
}
} }
} }

View file

@ -37,14 +37,15 @@ import java.util.List;
public class SuggestionSearchRunnable implements Runnable{ public class SuggestionSearchRunnable implements Runnable{
/**
* Runnable to update a {@link SuggestionListAdapter}
*/
private class SuggestionResultRunnable implements Runnable{ private class SuggestionResultRunnable implements Runnable{
private List<String> suggestions; private final List<String> suggestions;
private SuggestionListAdapter adapter;
private SuggestionResultRunnable(List<String> suggestions, SuggestionListAdapter adapter) { private SuggestionResultRunnable(List<String> suggestions) {
this.suggestions = suggestions; this.suggestions = suggestions;
this.adapter = adapter;
} }
@Override @Override
@ -55,9 +56,9 @@ public class SuggestionSearchRunnable implements Runnable{
private final int serviceId; private final int serviceId;
private final String query; private final String query;
final Handler h = new Handler(); private final Handler h = new Handler();
private Activity a = null; private final Activity a;
private SuggestionListAdapter adapter; private final SuggestionListAdapter adapter;
public SuggestionSearchRunnable(int serviceId, String query, public SuggestionSearchRunnable(int serviceId, String query,
Activity activity, SuggestionListAdapter adapter) { Activity activity, SuggestionListAdapter adapter) {
this.serviceId = serviceId; this.serviceId = serviceId;
@ -76,7 +77,7 @@ public class SuggestionSearchRunnable implements Runnable{
String searchLanguage = sp.getString(searchLanguageKey, String searchLanguage = sp.getString(searchLanguageKey,
a.getString(R.string.default_language_value)); a.getString(R.string.default_language_value));
List<String> suggestions = se.suggestionList(query, searchLanguage); List<String> suggestions = se.suggestionList(query, searchLanguage);
h.post(new SuggestionResultRunnable(suggestions, adapter)); h.post(new SuggestionResultRunnable(suggestions));
} catch (ExtractionException e) { } catch (ExtractionException e) {
ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content), ErrorActivity.reportError(h, a, e, null, a.findViewById(android.R.id.content),
ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED, ErrorActivity.ErrorInfo.make(ErrorActivity.SEARCHED,

View file

@ -5,6 +5,7 @@ import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import android.preference.PreferenceManager;
import android.support.annotation.LayoutRes; import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
@ -16,6 +17,8 @@ import android.view.ViewGroup;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.util.Objects;
/** /**
* Created by Christian Schabesberger on 31.08.15. * Created by Christian Schabesberger on 31.08.15.
@ -43,6 +46,10 @@ public class SettingsActivity extends PreferenceActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceBundle) { protected void onCreate(Bundle savedInstanceBundle) {
if (Objects.equals(PreferenceManager.getDefaultSharedPreferences(this)
.getString("theme", getResources().getString(R.string.light_theme_title)), getResources().getString(R.string.dark_theme_title))) {
setTheme(R.style.DarkTheme);
}
getDelegate().installViewFactory(); getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceBundle); getDelegate().onCreate(savedInstanceBundle);
super.onCreate(savedInstanceBundle); super.onCreate(savedInstanceBundle);

View file

@ -1,6 +1,7 @@
package org.schabi.newpipe.settings; package org.schabi.newpipe.settings;
import android.app.Activity; import android.app.Activity;
import android.app.ListActivity;
import android.content.ClipData; import android.content.ClipData;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -20,6 +21,7 @@ import org.schabi.newpipe.App;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Objects;
import info.guardianproject.netcipher.proxy.OrbotHelper; import info.guardianproject.netcipher.proxy.OrbotHelper;
@ -55,6 +57,7 @@ public class SettingsFragment extends PreferenceFragment
String DOWNLOAD_PATH_PREFERENCE; String DOWNLOAD_PATH_PREFERENCE;
String DOWNLOAD_PATH_AUDIO_PREFERENCE; String DOWNLOAD_PATH_AUDIO_PREFERENCE;
String USE_TOR_KEY; String USE_TOR_KEY;
String THEME;
public static final int REQUEST_INSTALL_ORBOT = 0x1234; public static final int REQUEST_INSTALL_ORBOT = 0x1234;
@ -63,11 +66,11 @@ public class SettingsFragment extends PreferenceFragment
private ListPreference searchLanguagePreference; private ListPreference searchLanguagePreference;
private Preference downloadPathPreference; private Preference downloadPathPreference;
private Preference downloadPathAudioPreference; private Preference downloadPathAudioPreference;
private Preference themePreference;
private SharedPreferences defaultPreferences; private SharedPreferences defaultPreferences;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings); addPreferencesFromResource(R.xml.settings);
@ -81,6 +84,7 @@ public class SettingsFragment extends PreferenceFragment
SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key); SEARCH_LANGUAGE_PREFERENCE = getString(R.string.search_language_key);
DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key); DOWNLOAD_PATH_PREFERENCE = getString(R.string.download_path_key);
DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key); DOWNLOAD_PATH_AUDIO_PREFERENCE = getString(R.string.download_path_audio_key);
THEME = getString(R.string.theme_key);
USE_TOR_KEY = getString(R.string.use_tor_key); USE_TOR_KEY = getString(R.string.use_tor_key);
// get pref objects // get pref objects
@ -92,6 +96,7 @@ public class SettingsFragment extends PreferenceFragment
(ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE); (ListPreference) findPreference(SEARCH_LANGUAGE_PREFERENCE);
downloadPathPreference = findPreference(DOWNLOAD_PATH_PREFERENCE); downloadPathPreference = findPreference(DOWNLOAD_PATH_PREFERENCE);
downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE); downloadPathAudioPreference = findPreference(DOWNLOAD_PATH_AUDIO_PREFERENCE);
themePreference = findPreference(THEME);
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override @Override
@ -132,6 +137,11 @@ public class SettingsFragment extends PreferenceFragment
downloadPathAudioPreference downloadPathAudioPreference
.setSummary(downloadPath); .setSummary(downloadPath);
} }
else if (key == THEME)
{
String theme = sharedPreferences.getString(THEME, "Light");
themePreference.setSummary(theme);
}
updateSummary(); updateSummary();
} }
}; };
@ -161,7 +171,6 @@ public class SettingsFragment extends PreferenceFragment
activity.startActivityForResult(i, R.string.download_path_audio_key); activity.startActivityForResult(i, R.string.download_path_audio_key);
} }
} }
return super.onPreferenceTreeClick(preferenceScreen, preference); return super.onPreferenceTreeClick(preferenceScreen, preference);
} }
@ -216,8 +225,8 @@ public class SettingsFragment extends PreferenceFragment
// installing the app does not necessarily return RESULT_OK // installing the app does not necessarily return RESULT_OK
App.configureTor(requestCode == REQUEST_INSTALL_ORBOT App.configureTor(requestCode == REQUEST_INSTALL_ORBOT
&& OrbotHelper.requestStartTor(a)); && OrbotHelper.requestStartTor(a));
} }
updateSummary(); updateSummary();
super.onActivityResult(requestCode, resultCode, data); super.onActivityResult(requestCode, resultCode, data);
} }
@ -239,6 +248,9 @@ public class SettingsFragment extends PreferenceFragment
downloadPathAudioPreference.setSummary( downloadPathAudioPreference.setSummary(
defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE, defaultPreferences.getString(DOWNLOAD_PATH_AUDIO_PREFERENCE,
getString(R.string.download_path_audio_summary))); getString(R.string.download_path_audio_summary)));
themePreference.setSummary(
defaultPreferences.getString(THEME,
getString(R.string.light_theme_title)));
} }
@Override @Override

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -3,12 +3,46 @@ package us.shandian.giga.get;
public interface DownloadManager public interface DownloadManager
{ {
int BLOCK_SIZE = 512 * 1024; 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); void resumeMission(int id);
/**
* Pause the execution of a download mission.
* @param id the identifier of the mission to pause.
*/
void pauseMission(int id); 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); 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); DownloadMission getMission(int id);
/**
* Get the number of download missions.
* @return the number of download missions.
*/
int getCount(); int getCount();
String getLocation();
} }

View file

@ -1,17 +1,21 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import android.content.Context; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.schabi.newpipe.settings.NewPipeSettings;
import java.io.File; import java.io.File;
import java.io.FilenameFilter;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; 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 us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
@ -19,33 +23,48 @@ import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadManagerImpl implements DownloadManager public class DownloadManagerImpl implements DownloadManager
{ {
private static final String TAG = DownloadManagerImpl.class.getSimpleName(); private static final String TAG = DownloadManagerImpl.class.getSimpleName();
private final DownloadDataSource mDownloadDataSource;
private Context mContext;
private String mLocation; private final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
/**
public DownloadManagerImpl(Context context, String location) { * Create a new instance
mContext = context; * @param searchLocations the directories to search for unfinished downloads
mLocation = location; * @param downloadDataSource the data source for finished downloads
loadMissions(); */
public DownloadManagerImpl(Collection<String> searchLocations, DownloadDataSource downloadDataSource) {
mDownloadDataSource = downloadDataSource;
loadMissions(searchLocations);
} }
@Override @Override
public int startMission(String url, String name, boolean isAudio, int threads) { public int startMission(String url, String location, String name, boolean isAudio, int threads) {
DownloadMission mission = new DownloadMission(); DownloadMission existingMission = getMissionByLocation(location, name);
mission.url = url; if(existingMission != null) {
mission.name = name; // Already downloaded or downloading
if(isAudio) { if(existingMission.finished) {
mission.location = NewPipeSettings.getAudioDownloadPath(mContext); // Overwrite mission
} else { deleteMission(mMissions.indexOf(existingMission));
mission.location = NewPipeSettings.getVideoDownloadPath(mContext); } 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.timestamp = System.currentTimeMillis();
mission.threadCount = threads; mission.threadCount = threads;
new Initializer(mContext, mission).start(); mission.addListener(new MissionListener(mission));
new Initializer(mission).start();
return insertMission(mission); return insertMission(mission);
} }
@Override @Override
public void resumeMission(int i) { public void resumeMission(int i) {
DownloadMission d = getMission(i); DownloadMission d = getMission(i);
@ -53,7 +72,7 @@ public class DownloadManagerImpl implements DownloadManager
d.start(); d.start();
} }
} }
@Override @Override
public void pauseMission(int i) { public void pauseMission(int i) {
DownloadMission d = getMission(i); DownloadMission d = getMission(i);
@ -61,55 +80,94 @@ public class DownloadManagerImpl implements DownloadManager
d.pause(); d.pause();
} }
} }
@Override @Override
public void deleteMission(int i) { public void deleteMission(int i) {
getMission(i).delete(); DownloadMission mission = getMission(i);
if(mission.finished) {
mDownloadDataSource.deleteMission(mission);
}
mission.delete();
mMissions.remove(i); mMissions.remove(i);
} }
private void loadMissions() { private void loadMissions(Iterable<String> searchLocations) {
File f = new File(mLocation); 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()) { if (f.exists() && f.isDirectory()) {
File[] subs = f.listFiles(); File[] subs = f.listFiles();
if(subs == null) {
Log.e(TAG, "listFiles() returned null");
return;
}
for (File sub : subs) { for (File sub : subs) {
if (sub.isDirectory()) { if (sub.isFile() && sub.getName().endsWith(".giga")) {
continue;
}
if (sub.getName().endsWith(".giga")) {
String str = Utility.readFromFile(sub.getAbsolutePath()); String str = Utility.readFromFile(sub.getAbsolutePath());
if (str != null && !str.trim().equals("")) { if (str != null && !str.trim().equals("")) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "loading mission " + sub.getName()); Log.d(TAG, "loading mission " + sub.getName());
Log.d(TAG, str); Log.d(TAG, str);
} }
DownloadMission mis = new Gson().fromJson(str, DownloadMission.class); DownloadMission mis = new Gson().fromJson(str, DownloadMission.class);
if (mis.finished) { if (mis.finished) {
sub.delete(); if(!sub.delete()) {
Log.w(TAG, "Unable to delete .giga file: " + sub.getPath());
}
continue; continue;
} }
mis.running = false; mis.running = false;
mis.recovered = true; mis.recovered = true;
insertMission(mis); 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; return i;
} }
@Override /**
public String getLocation() { * Get a mission by its location and name
return mLocation; * @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 class Initializer extends Thread {
private Context context;
private DownloadMission mission; private DownloadMission mission;
public Initializer(Context context, DownloadMission mission) { public Initializer(DownloadMission mission) {
this.context = context;
this.mission = 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) {
}
}
} }

View file

@ -1,6 +1,5 @@
package us.shandian.giga.get; package us.shandian.giga.get;
import android.content.Context;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.util.Log; import android.util.Log;
@ -10,39 +9,63 @@ import com.google.gson.Gson;
import java.io.File; import java.io.File;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import us.shandian.giga.util.Utility; import us.shandian.giga.util.Utility;
import static org.schabi.newpipe.BuildConfig.DEBUG; import static org.schabi.newpipe.BuildConfig.DEBUG;
public class DownloadMission public class DownloadMission
{ {
private static final String TAG = DownloadMission.class.getSimpleName(); private static final String TAG = DownloadMission.class.getSimpleName();
public interface MissionListener { public interface MissionListener {
HashMap<MissionListener, Handler> handlerStore = new HashMap<>(); HashMap<MissionListener, Handler> handlerStore = new HashMap<>();
void onProgressUpdate(long done, long total); void onProgressUpdate(DownloadMission downloadMission, long done, long total);
void onFinish(); void onFinish(DownloadMission downloadMission);
void onError(int errCode); void onError(DownloadMission downloadMission, int errCode);
} }
public static final int ERROR_SERVER_UNSUPPORTED = 206; public static final int ERROR_SERVER_UNSUPPORTED = 206;
public static final int ERROR_UNKNOWN = 233; public static final int ERROR_UNKNOWN = 233;
public String name = ""; /**
public String url = ""; * The filename
public String location = ""; */
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; public long blocks;
/**
* Number of bytes
*/
public long length; public long length;
/**
* Number of bytes downloaded
*/
public long done; public long done;
public int threadCount = 3; public int threadCount = 3;
public int finishCount; public int finishCount;
public List<Long> threadPositions = new ArrayList<Long>(); private List<Long> threadPositions = new ArrayList<Long>();
public Map<Long, Boolean> blockState = new HashMap<Long, Boolean>(); public final Map<Long, Boolean> blockState = new HashMap<Long, Boolean>();
public boolean running; public boolean running;
public boolean finished; public boolean finished;
public boolean fallback; public boolean fallback;
@ -53,23 +76,65 @@ public class DownloadMission
private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>(); private transient ArrayList<WeakReference<MissionListener>> mListeners = new ArrayList<WeakReference<MissionListener>>();
private transient boolean mWritingToFile; 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) { public boolean isBlockPreserved(long block) {
checkBlock(block);
return blockState.containsKey(block) ? blockState.get(block) : false; return blockState.containsKey(block) ? blockState.get(block) : false;
} }
public void preserveBlock(long block) { public void preserveBlock(long block) {
checkBlock(block);
synchronized (blockState) { synchronized (blockState) {
blockState.put(block, true); 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) { public synchronized void notifyProgress(long deltaLen) {
@ -95,13 +160,16 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { 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() { public synchronized void notifyFinished() {
if (errCode > 0) return; if (errCode > 0) return;
@ -111,7 +179,10 @@ public class DownloadMission
onFinish(); onFinish();
} }
} }
/**
* Called when all parts are downloaded
*/
private void onFinish() { private void onFinish() {
if (errCode > 0) return; if (errCode > 0) return;
@ -130,7 +201,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { public void run() {
listener.onFinish(); listener.onFinish(DownloadMission.this);
} }
}); });
} }
@ -147,7 +218,7 @@ public class DownloadMission
MissionListener.handlerStore.get(listener).post(new Runnable() { MissionListener.handlerStore.get(listener).post(new Runnable() {
@Override @Override
public void run() { 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() { public void start() {
if (!running && !finished) { if (!running && !finished) {
running = true; running = true;
@ -200,12 +274,19 @@ public class DownloadMission
// if (err) // if (err)
} }
} }
/**
* Removes the file and the meta file
*/
public void delete() { public void delete() {
deleteThisFromFile(); 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() { public void writeThisToFile() {
if (!mWritingToFile) { if (!mWritingToFile) {
mWritingToFile = true; mWritingToFile = true;
@ -218,14 +299,30 @@ public class DownloadMission
}.start(); }.start();
} }
} }
/**
* Write this {@link DownloadMission} to the meta file.
*/
private void doWriteThisToFile() { private void doWriteThisToFile() {
synchronized (blockState) { synchronized (blockState) {
Utility.writeToFile(location + "/" + name + ".giga", new Gson().toJson(this)); Utility.writeToFile(getMetaFilename(), new Gson().toJson(this));
} }
} }
private void deleteThisFromFile() { 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);
}
} }

View file

@ -9,14 +9,19 @@ import java.net.URL;
import static org.schabi.newpipe.BuildConfig.DEBUG; 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 public class DownloadRunnable implements Runnable
{ {
private static final String TAG = DownloadRunnable.class.getSimpleName(); private static final String TAG = DownloadRunnable.class.getSimpleName();
private DownloadMission mMission; private final DownloadMission mMission;
private int mId; private final int mId;
public DownloadRunnable(DownloadMission mission, int id) { public DownloadRunnable(DownloadMission mission, int id) {
if(mission == null) throw new NullPointerException("mission is null");
mMission = mission; mMission = mission;
mId = id; mId = id;
} }
@ -86,7 +91,7 @@ public class DownloadRunnable implements Runnable
Log.d(TAG, mId + ":Content-Length=" + conn.getContentLength() + " Code:" + conn.getResponseCode()); 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) { if (conn.getResponseCode() != 206) {
mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED; mMission.errCode = DownloadMission.ERROR_SERVER_UNSUPPORTED;
notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED); notifyError(DownloadMission.ERROR_SERVER_UNSUPPORTED);
@ -131,7 +136,7 @@ public class DownloadRunnable implements Runnable
notifyProgress(-total); notifyProgress(-total);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, mId + ":position " + position + " retrying"); Log.d(TAG, mId + ":position " + position + " retrying", e);
} }
} }
} }

View file

@ -8,10 +8,11 @@ import java.net.URL;
// Single-threaded fallback mode // Single-threaded fallback mode
public class DownloadRunnableFallback implements Runnable public class DownloadRunnableFallback implements Runnable
{ {
private DownloadMission mMission; private final DownloadMission mMission;
//private int mId; //private int mId;
public DownloadRunnableFallback(DownloadMission mission) { public DownloadRunnableFallback(DownloadMission mission) {
if(mission == null) throw new NullPointerException("mission is null");
//mId = id; //mId = id;
mMission = mission; mMission = mission;
} }
@ -35,7 +36,7 @@ public class DownloadRunnableFallback implements Runnable
f.write(buf, 0, len); f.write(buf, 0, len);
notifyProgress(len); notifyProgress(len);
if (Thread.currentThread().interrupted()) { if (Thread.interrupted()) {
break; break;
} }

View file

@ -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 = "downloads.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;
}
}

View file

@ -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});
}
}

View file

@ -1,56 +1,96 @@
package us.shandian.giga.service; package us.shandian.giga.service;
import android.Manifest;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Binder; import android.os.Binder;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.IBinder; import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.content.PermissionChecker;
import android.util.Log; import android.util.Log;
import android.widget.Toast;
import org.schabi.newpipe.R;
import org.schabi.newpipe.download.DownloadActivity; import org.schabi.newpipe.download.DownloadActivity;
import org.schabi.newpipe.settings.NewPipeSettings; 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.DownloadManager;
import us.shandian.giga.get.DownloadManagerImpl; import us.shandian.giga.get.DownloadManagerImpl;
import us.shandian.giga.get.DownloadMission; import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.sqlite.SQLiteDownloadDataSource;
import static org.schabi.newpipe.BuildConfig.DEBUG; 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(); 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 DMBinder mBinder;
private DownloadManager mManager; private DownloadManager mManager;
private Notification mNotification; private Notification mNotification;
private Handler mHandler; private Handler mHandler;
private long mLastTimeStamp = System.currentTimeMillis(); 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 @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "onCreate"); Log.d(TAG, "onCreate");
} }
mBinder = new DMBinder(); mBinder = new DMBinder();
if(mDataSource == null) {
mDataSource = new SQLiteDownloadDataSource(this);
}
if (mManager == null) { if (mManager == null) {
String path = NewPipeSettings.getVideoDownloadPath(this); ArrayList<String> paths = new ArrayList<>(2);
mManager = new DownloadManagerImpl(this, path); paths.add(NewPipeSettings.getVideoDownloadPath(this));
paths.add(NewPipeSettings.getAudioDownloadPath(this));
mManager = new DownloadManagerImpl(paths, mDataSource);
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "mManager == null"); Log.d(TAG, "mManager == null");
Log.d(TAG, "Download directory: " + path); Log.d(TAG, "Download directory: " + paths);
} }
} }
Intent i = new Intent(); Intent i = new Intent();
i.setAction(Intent.ACTION_MAIN); i.setAction(Intent.ACTION_MAIN);
i.setClass(this, DownloadActivity.class); i.setClass(this, DownloadActivity.class);
@ -83,28 +123,50 @@ public class DownloadManagerService extends Service implements DownloadMission.M
mHandler = new Handler(thread.getLooper()) { mHandler = new Handler(thread.getLooper()) {
@Override @Override
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
if (msg.what == 0) { switch (msg.what) {
int runningCount = 0; case UPDATE_MESSAGE: {
int runningCount = 0;
for (int i = 0; i < mManager.getCount(); i++) {
if (mManager.getMission(i).running) { for (int i = 0; i < mManager.getCount(); i++) {
runningCount++; 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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "Starting"); 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; return START_NOT_STICKY;
} }
@ -119,52 +181,76 @@ public class DownloadManagerService extends Service implements DownloadMission.M
for (int i = 0; i < mManager.getCount(); i++) { for (int i = 0; i < mManager.getCount(); i++) {
mManager.pauseMission(i); mManager.pauseMission(i);
} }
stopForeground(true); stopForeground(true);
} }
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
int permissionCheck;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
permissionCheck = PermissionChecker.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
if(permissionCheck == PermissionChecker.PERMISSION_DENIED) {
Toast.makeText(this, "Permission denied (read)", Toast.LENGTH_SHORT).show();
}
}
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; 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() { private void postUpdateMessage() {
mHandler.sendEmptyMessage(0); mHandler.sendEmptyMessage(UPDATE_MESSAGE);
} }
private void updateState(int runningCount) { private void updateState(int runningCount) {
if (runningCount == 0) { if (runningCount == 0) {
stopForeground(true); stopForeground(true);
} else { } 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 // Wrapper of DownloadManager
public class DMBinder extends Binder { public class DMBinder extends Binder {
public DownloadManager getDownloadManager() { public DownloadManager getDownloadManager() {
@ -172,15 +258,13 @@ public class DownloadManagerService extends Service implements DownloadMission.M
} }
public void onMissionAdded(DownloadMission mission) { public void onMissionAdded(DownloadMission mission) {
mission.addListener(DownloadManagerService.this); mission.addListener(missionListener);
postUpdateMessage(); postUpdateMessage();
} }
public void onMissionRemoved(DownloadMission mission) { public void onMissionRemoved(DownloadMission mission) {
mission.removeListener(DownloadManagerService.this); mission.removeListener(missionListener);
postUpdateMessage(); postUpdateMessage();
} }
} }
} }

View file

@ -5,6 +5,9 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; 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.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -19,6 +22,7 @@ import android.support.v7.widget.RecyclerView;
import java.io.File; import java.io.File;
import java.util.HashMap; import java.util.HashMap;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
@ -28,10 +32,14 @@ import us.shandian.giga.service.DownloadManagerService;
import us.shandian.giga.ui.common.ProgressDrawable; import us.shandian.giga.ui.common.ProgressDrawable;
import us.shandian.giga.util.Utility; 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> public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHolder>
{ {
private static final Map<Integer, String> ALGORITHMS = new HashMap<>(); private static final Map<Integer, String> ALGORITHMS = new HashMap<>();
private static final String TAG = "MissionAdapter";
static { static {
ALGORITHMS.put(R.id.md5, "MD5"); ALGORITHMS.put(R.id.md5, "MD5");
ALGORITHMS.put(R.id.sha1, "SHA1"); ALGORITHMS.put(R.id.sha1, "SHA1");
@ -143,9 +151,8 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
h.status.setText(R.string.msg_error); h.status.setText(R.string.msg_error);
} else { } else {
float progress = (float) h.mission.done / h.mission.length; float progress = (float) h.mission.done / h.mission.length;
h.status.setText(String.format("%.2f%%", progress * 100)); h.status.setText(String.format(Locale.US, "%.2f%%", progress * 100));
h.progress.setProgress(progress); h.progress.setProgress(progress);
} }
} }
@ -212,23 +219,23 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
h.lastDone = -1; h.lastDone = -1;
return true; return true;
case R.id.view: case R.id.view:
Intent i = new Intent(); File f = new File(h.mission.location, h.mission.name);
i.setAction(Intent.ACTION_VIEW);
File f = new File(h.mission.location + "/" + h.mission.name);
String ext = Utility.getFileExt(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)); String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext.substring(1));
Log.v(TAG, "Mime: " + mime + " package: " + mContext.getApplicationContext().getPackageName() + ".provider");
if (f.exists()) { if (f.exists()) {
i.setDataAndType(Uri.fromFile(f), mime); viewFileWithFileProvider(f, mime);
} else {
try { Log.w(TAG, "File doesn't exist");
mContext.startActivity(i);
} catch (Exception e) {
}
} }
return true; return true;
@ -249,6 +256,34 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
popup.show(); 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> { private class ChecksumTask extends AsyncTask<String, Void, String> {
ProgressDialog prog; ProgressDialog prog;
@ -280,7 +315,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
static class ViewHolder extends RecyclerView.ViewHolder { static class ViewHolder extends RecyclerView.ViewHolder {
public DownloadMission mission; public DownloadMission mission;
public int position; public int position;
public TextView status; public TextView status;
public ImageView icon; public ImageView icon;
public TextView name; public TextView name;
@ -289,7 +324,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
public ImageView menu; public ImageView menu;
public ProgressDrawable progress; public ProgressDrawable progress;
public MissionObserver observer; public MissionObserver observer;
public long lastTimeStamp = -1; public long lastTimeStamp = -1;
public long lastDone = -1; public long lastDone = -1;
public int colorId; public int colorId;
@ -316,12 +351,12 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
} }
@Override @Override
public void onProgressUpdate(long done, long total) { public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
mAdapter.updateProgress(mHolder); mAdapter.updateProgress(mHolder);
} }
@Override @Override
public void onFinish() { public void onFinish(DownloadMission downloadMission) {
//mAdapter.mManager.deleteMission(mHolder.position); //mAdapter.mManager.deleteMission(mHolder.position);
// TODO Notification // TODO Notification
//mAdapter.notifyDataSetChanged(); //mAdapter.notifyDataSetChanged();
@ -332,7 +367,7 @@ public class MissionAdapter extends RecyclerView.Adapter<MissionAdapter.ViewHold
} }
@Override @Override
public void onError(int errCode) { public void onError(DownloadMission downloadMission, int errCode) {
mAdapter.updateProgress(mHolder); mAdapter.updateProgress(mHolder);
} }

View file

@ -40,7 +40,7 @@ public abstract class MissionsFragment extends Fragment
private MissionAdapter mAdapter; private MissionAdapter mAdapter;
private GridLayoutManager mGridManager; private GridLayoutManager mGridManager;
private LinearLayoutManager mLinearManager; private LinearLayoutManager mLinearManager;
private Activity mActivity; private Context mActivity;
private ServiceConnection mConnection = new ServiceConnection() { private ServiceConnection mConnection = new ServiceConnection() {
@ -65,7 +65,7 @@ public abstract class MissionsFragment extends Fragment
mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
mLinear = mPrefs.getBoolean("linear", false); mLinear = mPrefs.getBoolean("linear", false);
// Bind the service // Bind the service
Intent i = new Intent(); Intent i = new Intent();
i.setClass(getActivity(), DownloadManagerService.class); i.setClass(getActivity(), DownloadManagerService.class);
@ -85,14 +85,15 @@ public abstract class MissionsFragment extends Fragment
} }
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Context activity) {
super.onAttach(activity); super.onAttach(activity);
mActivity = activity; mActivity = activity;
} }
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onDestroyView() {
super.onDestroyView();
getActivity().unbindService(mConnection);
} }
@Override @Override

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Some files were not shown because too many files have changed in this diff Show more