Merge remote-tracking branch 'origin/master'
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
|
||||||
arguments.getString(VIDEO_URL),
|
|
||||||
fName + arguments.getString(FILE_SUFFIX_VIDEO),
|
|
||||||
audioButton.isChecked(),
|
|
||||||
threads.getProgress() + 1);
|
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,
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
package org.schabi.newpipe.download;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import us.shandian.giga.get.DownloadMission;
|
|
||||||
import us.shandian.giga.get.DownloadMission.MissionListener;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by erwin on 06.11.16.
|
|
||||||
*
|
|
||||||
* Copyright (C) Christian Schabesberger 2016 <chris.schabesberger@mailbox.org>
|
|
||||||
* DownloadListener.java is part of NewPipe.
|
|
||||||
*
|
|
||||||
* NewPipe is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* NewPipe is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
class DownloadListener implements MissionListener
|
|
||||||
{
|
|
||||||
DownloadMission mMission;
|
|
||||||
Context mContext;
|
|
||||||
|
|
||||||
public DownloadListener(Context context, DownloadMission mission)
|
|
||||||
{
|
|
||||||
super();
|
|
||||||
mMission = mission;
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onProgressUpdate(long done, long total)
|
|
||||||
{
|
|
||||||
// do nothing special ...
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinish()
|
|
||||||
{
|
|
||||||
// notify media scanner on downloaded media file ...
|
|
||||||
mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
|
|
||||||
Uri.parse( "file://" + mMission.location
|
|
||||||
+ "/" + mMission.name)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(int errCode)
|
|
||||||
{
|
|
||||||
// do nothing special ...
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,7 +42,7 @@ import info.guardianproject.netcipher.NetCipher;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
// TODO: FOR HEVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!!
|
// TODO: FOR HEAVEN SAKE !!! DO NOT SIMPLY USE ASYNCTASK. MAKE THIS A PROPER SERVICE !!!
|
||||||
public class FileDownloader extends AsyncTask<Void, Integer, Void> {
|
public class FileDownloader extends AsyncTask<Void, Integer, Void> {
|
||||||
public static final String TAG = "FileDownloader";
|
public static final String TAG = "FileDownloader";
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
String lowercaseUrl = url.toLowerCase();
|
||||||
|
if(lowercaseUrl.contains("youtube")) {
|
||||||
if (url.contains("attribution_link")) {
|
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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
Bundle args = getArguments();
|
||||||
|
if(args != null) {
|
||||||
|
searchQuery = args.getString(QUERY);
|
||||||
|
streamingServiceId = args.getInt(STREAMING_SERVICE);
|
||||||
|
} else {
|
||||||
streamingServiceId = NewPipe.getIdOfService("Youtube");
|
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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
|
||||||
|
public class PermissionHelper {
|
||||||
|
public static final int PERMISSION_WRITE_STORAGE = 778;
|
||||||
|
public static final int PERMISSION_READ_STORAGE = 777;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean checkStoragePermissions(Activity activity) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
if(!checkReadStoragePermissions(activity)) return false;
|
||||||
|
}
|
||||||
|
return checkWriteStoragePermissions(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
public static boolean checkReadStoragePermissions(Activity activity) {
|
||||||
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
ActivityCompat.requestPermissions(activity,
|
||||||
|
new String[]{
|
||||||
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
PERMISSION_READ_STORAGE);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static boolean checkWriteStoragePermissions(Activity activity) {
|
||||||
|
// Here, thisActivity is the current activity
|
||||||
|
if (ContextCompat.checkSelfPermission(activity,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
|
||||||
|
// Should we show an explanation?
|
||||||
|
/*if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
|
||||||
|
// Show an explanation to the user *asynchronously* -- don't block
|
||||||
|
// this thread waiting for the user's response! After the user
|
||||||
|
// sees the explanation, try again to request the permission.
|
||||||
|
} else {*/
|
||||||
|
|
||||||
|
// No explanation needed, we can request the permission.
|
||||||
|
ActivityCompat.requestPermissions(activity,
|
||||||
|
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
|
PERMISSION_WRITE_STORAGE);
|
||||||
|
|
||||||
|
// PERMISSION_WRITE_STORAGE is an
|
||||||
|
// app-defined int constant. The callback method gets the
|
||||||
|
// result of the request.
|
||||||
|
/*}*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package us.shandian.giga.get;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides access to the storage of {@link DownloadMission}s
|
||||||
|
*/
|
||||||
|
public interface DownloadDataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all missions
|
||||||
|
* @return a list of download missions
|
||||||
|
*/
|
||||||
|
List<DownloadMission> loadMissions();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a downlaod mission to the storage
|
||||||
|
* @param downloadMission the download mission to add
|
||||||
|
* @return the identifier of the mission
|
||||||
|
*/
|
||||||
|
void addMission(DownloadMission downloadMission);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a download mission which exists in the storage
|
||||||
|
* @param downloadMission the download mission to update
|
||||||
|
* @throws IllegalArgumentException if the mission was not added to storage
|
||||||
|
*/
|
||||||
|
void updateMission(DownloadMission downloadMission);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a download mission
|
||||||
|
* @param downloadMission the mission to delete
|
||||||
|
*/
|
||||||
|
void deleteMission(DownloadMission downloadMission);
|
||||||
|
}
|
|
@ -4,11 +4,45 @@ 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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,30 +23,45 @@ 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 final ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
|
||||||
private String mLocation;
|
|
||||||
protected ArrayList<DownloadMission> mMissions = new ArrayList<DownloadMission>();
|
|
||||||
|
|
||||||
public DownloadManagerImpl(Context context, String location) {
|
/**
|
||||||
mContext = context;
|
* Create a new instance
|
||||||
mLocation = location;
|
* @param searchLocations the directories to search for unfinished downloads
|
||||||
loadMissions();
|
* @param downloadDataSource the data source for finished downloads
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
deleteMission(mMissions.indexOf(existingMission));
|
||||||
} else {
|
} else {
|
||||||
mission.location = NewPipeSettings.getVideoDownloadPath(mContext);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,22 +83,70 @@ public class DownloadManagerImpl implements DownloadManager
|
||||||
|
|
||||||
@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();
|
||||||
|
|
||||||
for (File sub : subs) {
|
if(subs == null) {
|
||||||
if (sub.isDirectory()) {
|
Log.e(TAG, "listFiles() returned null");
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub.getName().endsWith(".giga")) {
|
for (File sub : subs) {
|
||||||
|
if (sub.isFile() && 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("")) {
|
||||||
|
|
||||||
|
@ -91,7 +158,9 @@ public class DownloadManagerImpl implements DownloadManager
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,17 +168,6 @@ public class DownloadManagerImpl implements DownloadManager
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,17 +203,80 @@ public class DownloadManagerImpl implements DownloadManager
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a mission by its location and name
|
||||||
|
* @param location the location
|
||||||
|
* @param name the name
|
||||||
|
* @return the mission or null if no such mission exists
|
||||||
|
*/
|
||||||
|
private @Nullable DownloadMission getMissionByLocation(String location, String name) {
|
||||||
|
for(DownloadMission mission: mMissions) {
|
||||||
|
if(location.equals(mission.location) && name.equals(mission.name)) {
|
||||||
|
return mission;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the filename into name and extension
|
||||||
|
*
|
||||||
|
* Dots are ignored if they appear: not at all, at the beginning of the file,
|
||||||
|
* at the end of the file
|
||||||
|
*
|
||||||
|
* @param name the name to split
|
||||||
|
* @return a string array with a length of 2 containing the name and the extension
|
||||||
|
*/
|
||||||
|
private static String[] splitName(String name) {
|
||||||
|
int dotIndex = name.lastIndexOf('.');
|
||||||
|
if(dotIndex <= 0 || (dotIndex == name.length() - 1)) {
|
||||||
|
return new String[]{name, ""};
|
||||||
|
} else {
|
||||||
|
return new String[]{name.substring(0, dotIndex), name.substring(dotIndex + 1)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique file name.
|
||||||
|
*
|
||||||
|
* e.g. "myname (1).txt" if the name "myname.txt" exists.
|
||||||
|
* @param location the location (to check for existing files)
|
||||||
|
* @param name the name of the file
|
||||||
|
* @return the unique file name
|
||||||
|
* @throws IllegalArgumentException if the location is not a directory
|
||||||
|
* @throws SecurityException if the location is not readable
|
||||||
|
*/
|
||||||
|
private static String generateUniqueName(String location, String name) {
|
||||||
|
if(location == null) throw new NullPointerException("location is null");
|
||||||
|
if(name == null) throw new NullPointerException("name is null");
|
||||||
|
File destination = new File(location);
|
||||||
|
if(!destination.isDirectory()) {
|
||||||
|
throw new IllegalArgumentException("location is not a directory: " + location);
|
||||||
|
}
|
||||||
|
final String[] nameParts = splitName(name);
|
||||||
|
String[] existingName = destination.list(new FilenameFilter() {
|
||||||
@Override
|
@Override
|
||||||
public String getLocation() {
|
public boolean accept(File dir, String name) {
|
||||||
return mLocation;
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,12 +9,13 @@ 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
|
||||||
|
@ -25,24 +25,47 @@ public class DownloadMission
|
||||||
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;
|
||||||
|
@ -54,22 +77,64 @@ 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;
|
||||||
|
|
||||||
|
@ -112,6 +180,9 @@ public class DownloadMission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -170,6 +241,9 @@ public class DownloadMission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start downloading with multiple threads.
|
||||||
|
*/
|
||||||
public void start() {
|
public void start() {
|
||||||
if (!running && !finished) {
|
if (!running && !finished) {
|
||||||
running = true;
|
running = true;
|
||||||
|
@ -201,11 +275,18 @@ public class DownloadMission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
@ -219,13 +300,29 @@ public class DownloadMission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,37 +1,72 @@
|
||||||
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() {
|
||||||
|
@ -42,12 +77,17 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +123,8 @@ 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) {
|
||||||
|
case UPDATE_MESSAGE: {
|
||||||
int runningCount = 0;
|
int runningCount = 0;
|
||||||
|
|
||||||
for (int i = 0; i < mManager.getCount(); i++) {
|
for (int i = 0; i < mManager.getCount(); i++) {
|
||||||
|
@ -91,20 +132,41 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||||
runningCount++;
|
runningCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState(runningCount);
|
updateState(runningCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,17 +187,51 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||||
|
|
||||||
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void postUpdateMessage() {
|
||||||
|
mHandler.sendEmptyMessage(UPDATE_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState(int runningCount) {
|
||||||
|
if (runningCount == 0) {
|
||||||
|
stopForeground(true);
|
||||||
|
} else {
|
||||||
|
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
|
@Override
|
||||||
public void onProgressUpdate(long done, long total) {
|
public void onProgressUpdate(DownloadMission downloadMission, long done, long total) {
|
||||||
|
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
long delta = now - mLastTimeStamp;
|
long delta = now - mLastTimeStamp;
|
||||||
|
|
||||||
if (delta > 2000) {
|
if (delta > 2000) {
|
||||||
postUpdateMessage();
|
postUpdateMessage();
|
||||||
mLastTimeStamp = now;
|
mLastTimeStamp = now;
|
||||||
|
@ -143,25 +239,15 @@ public class DownloadManagerService extends Service implements DownloadMission.M
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFinish() {
|
public void onFinish(DownloadMission downloadMission) {
|
||||||
postUpdateMessage();
|
postUpdateMessage();
|
||||||
|
notifyMediaScanner(downloadMission);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(int errCode) {
|
public void onError(DownloadMission downloadMission, int errCode) {
|
||||||
postUpdateMessage();
|
postUpdateMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void postUpdateMessage() {
|
|
||||||
mHandler.sendEmptyMessage(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateState(int runningCount) {
|
|
||||||
if (runningCount == 0) {
|
|
||||||
stopForeground(true);
|
|
||||||
} else {
|
|
||||||
startForeground(1000, mNotification);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,9 +32,13 @@ 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");
|
||||||
|
@ -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;
|
||||||
|
@ -250,6 +257,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;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
After Width: | Height: | Size: 384 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 148 B |
After Width: | Height: | Size: 163 B |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 397 B |
After Width: | Height: | Size: 269 B |
After Width: | Height: | Size: 280 B |
After Width: | Height: | Size: 262 B |
After Width: | Height: | Size: 277 B |
After Width: | Height: | Size: 249 B |
After Width: | Height: | Size: 251 B |
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 116 B |
After Width: | Height: | Size: 230 B |
After Width: | Height: | Size: 238 B |
After Width: | Height: | Size: 286 B |
After Width: | Height: | Size: 268 B |
After Width: | Height: | Size: 195 B |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 181 B |
After Width: | Height: | Size: 188 B |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 449 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 144 B |
After Width: | Height: | Size: 157 B |
After Width: | Height: | Size: 412 B |
After Width: | Height: | Size: 433 B |
After Width: | Height: | Size: 532 B |
After Width: | Height: | Size: 496 B |
After Width: | Height: | Size: 281 B |
After Width: | Height: | Size: 296 B |
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 307 B |
After Width: | Height: | Size: 649 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 173 B |
After Width: | Height: | Size: 197 B |
After Width: | Height: | Size: 586 B |
After Width: | Height: | Size: 610 B |
After Width: | Height: | Size: 768 B |
After Width: | Height: | Size: 698 B |
After Width: | Height: | Size: 395 B |
After Width: | Height: | Size: 423 B |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 434 B |
After Width: | Height: | Size: 869 B |
After Width: | Height: | Size: 883 B |
After Width: | Height: | Size: 209 B |
After Width: | Height: | Size: 233 B |
After Width: | Height: | Size: 786 B |
After Width: | Height: | Size: 838 B |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 938 B |
After Width: | Height: | Size: 496 B |
After Width: | Height: | Size: 533 B |
After Width: | Height: | Size: 510 B |