mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-01-14 02:32:40 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92db9cb59b | ||
|
|
410c4ca736 | ||
|
|
80c9dbf180 | ||
|
|
c9edac2820 | ||
|
|
143df9a529 | ||
|
|
c87da9903f | ||
|
|
0f69e6c64d | ||
|
|
999efb6660 | ||
|
|
0b391a9ef3 | ||
|
|
2a99ac4430 | ||
|
|
a5589d0865 | ||
|
|
83541a0d5d | ||
|
|
ac0dff7aa1 | ||
|
|
f22b5157f5 | ||
|
|
659d0d6115 | ||
|
|
fd8c99fd8d | ||
|
|
9494f3a299 | ||
|
|
8021848b03 | ||
|
|
5a127c26e6 | ||
|
|
a7d734c20c | ||
|
|
7c7129f9a1 | ||
|
|
05cbc7891d | ||
|
|
14623456ff | ||
|
|
5064ec3ac4 | ||
|
|
892888796d | ||
|
|
4f2826d2c2 | ||
|
|
7f824d725b | ||
|
|
da4096c4ef |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@
|
||||
/.idea
|
||||
/*.iml
|
||||
gradle.properties
|
||||
*~
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId "org.schabi.newpipe"
|
||||
minSdkVersion 15
|
||||
targetSdkVersion 25
|
||||
versionCode 20
|
||||
versionName "0.8.6"
|
||||
versionCode 21
|
||||
versionName "0.8.7"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
@@ -32,10 +32,10 @@ android {
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
compile 'com.android.support:appcompat-v7:25.0.0'
|
||||
compile 'com.android.support:support-v4:25.0.0'
|
||||
compile 'com.android.support:design:25.0.0'
|
||||
compile 'com.android.support:recyclerview-v7:25.0.0'
|
||||
compile 'com.android.support:appcompat-v7:25.1.0'
|
||||
compile 'com.android.support:support-v4:25.1.0'
|
||||
compile 'com.android.support:design:25.1.0'
|
||||
compile 'com.android.support:recyclerview-v7:25.1.0'
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'org.mozilla:rhino:1.7.7'
|
||||
compile 'info.guardianproject.netcipher:netcipher:1.2'
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.schabi.newpipe;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.NavUtils;
|
||||
@@ -34,6 +35,8 @@ import android.webkit.WebViewClient;
|
||||
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
public class ReCaptchaActivity extends AppCompatActivity {
|
||||
public static final int RECAPTCHA_REQUEST = 10;
|
||||
|
||||
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||
public static final String YT_URL = "https://www.youtube.com";
|
||||
|
||||
@@ -42,6 +45,9 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_recaptcha);
|
||||
|
||||
// Set return to Cancel by default
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.reCaptcha_title);
|
||||
@@ -80,19 +86,26 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
||||
context = ctx;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
// TODO: Start Loader
|
||||
super.onPageStarted(view, url, favicon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
String cookies = CookieManager.getInstance().getCookie(url);
|
||||
|
||||
// TODO: Stop Loader
|
||||
|
||||
// find cookies : s_gl & goojf and Add cookies to Downloader
|
||||
if (find_access_cookies(cookies)) {
|
||||
// Give cookies to Downloader class
|
||||
Downloader.setCookies(mCookies);
|
||||
|
||||
// Closing activity and return to parent.
|
||||
Intent intent = new Intent(context, org.schabi.newpipe.MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
NavUtils.navigateUpTo(context, intent);
|
||||
// Closing activity and return to parent
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.os.Handler;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.extractor.exceptions.ParsingException;
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamExtractor;
|
||||
import org.schabi.newpipe.extractor.stream_info.StreamInfo;
|
||||
import org.schabi.newpipe.report.ErrorActivity;
|
||||
@@ -44,6 +44,7 @@ public class StreamInfoWorker {
|
||||
public interface OnStreamInfoReceivedListener {
|
||||
void onReceive(StreamInfo info);
|
||||
void onError(int messageId);
|
||||
void onReCaptchaException();
|
||||
void onBlockedByGemaError();
|
||||
void onContentErrorWithMessage(int messageId);
|
||||
void onContentError();
|
||||
@@ -107,6 +108,13 @@ public class StreamInfoWorker {
|
||||
}
|
||||
|
||||
// These errors render the stream information unusable.
|
||||
} catch (ReCaptchaException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
onStreamInfoReceivedListener.onReCaptchaException();
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
@@ -115,9 +123,8 @@ public class StreamInfoWorker {
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
}
|
||||
// custom service related exceptions
|
||||
catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
} catch (YoutubeStreamExtractor.DecryptException de) {
|
||||
// custom service related exceptions
|
||||
h.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.util.Vector;
|
||||
|
||||
import org.schabi.newpipe.ActivityCommunicator;
|
||||
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;
|
||||
@@ -59,6 +60,9 @@ import org.schabi.newpipe.player.BackgroundPlayer;
|
||||
import org.schabi.newpipe.player.PlayVideoActivity;
|
||||
import org.schabi.newpipe.player.ExoPlayerActivity;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||
@@ -579,11 +583,6 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||
* fragment (e.g. upon screen orientation changes).
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -604,6 +603,17 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
postNewErrorToast(messageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReCaptchaException() {
|
||||
Toast.makeText(getActivity(), R.string.recaptcha_request_toast,
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
startActivityForResult(
|
||||
new Intent(getActivity(), ReCaptchaActivity.class),
|
||||
RECAPTCHA_REQUEST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlockedByGemaError() {
|
||||
onErrorBlockedByGema();
|
||||
@@ -793,4 +803,23 @@ public class VideoItemDetailFragment extends Fragment {
|
||||
VideoItemDetailFragment.STREAMING_SERVICE, streamingServiceId);
|
||||
activity.startActivity(detailIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
String videoUrl = getArguments().getString(VIDEO_URL);
|
||||
StreamInfoWorker siw = StreamInfoWorker.getInstance();
|
||||
siw.search(streamingServiceId, videoUrl, getActivity());
|
||||
} else {
|
||||
Log.d(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,14 @@ public class AudioStream {
|
||||
this.bandwidth = bandwidth; this.sampling_rate = samplingRate;
|
||||
}
|
||||
|
||||
// reveals wether two streams are the same, but have diferent urls
|
||||
// reveals whether two streams are the same, but have different urls
|
||||
public boolean equalStats(AudioStream cmp) {
|
||||
return format == cmp.format
|
||||
&& bandwidth == cmp.bandwidth
|
||||
&& sampling_rate == cmp.sampling_rate;
|
||||
}
|
||||
|
||||
// revelas wether two streams are equal
|
||||
// reveals whether two streams are equal
|
||||
public boolean equals(AudioStream cmp) {
|
||||
return cmp != null && equalStats(cmp)
|
||||
&& url == cmp.url;
|
||||
|
||||
@@ -37,14 +37,14 @@ public abstract class StreamExtractor {
|
||||
private UrlIdHandler urlIdHandler;
|
||||
private StreamPreviewInfoCollector previewInfoCollector;
|
||||
|
||||
public class ExctractorInitException extends ExtractionException {
|
||||
public ExctractorInitException(String message) {
|
||||
public class ExtractorInitException extends ExtractionException {
|
||||
public ExtractorInitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
public ExctractorInitException(Throwable cause) {
|
||||
public ExtractorInitException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public ExctractorInitException(String message, Throwable cause) {
|
||||
public ExtractorInitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ public class StreamInfo extends AbstractStreamInfo {
|
||||
private static StreamInfo extractImportantData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor)
|
||||
throws ExtractionException, IOException {
|
||||
/* ---- importand data, withoug the video can't be displayed goes here: ---- */
|
||||
// if one of these is not available an exception is ment to be thrown directly into the frontend.
|
||||
/* ---- important data, withoug the video can't be displayed goes here: ---- */
|
||||
// if one of these is not available an exception is meant to be thrown directly into the frontend.
|
||||
|
||||
UrlIdHandler uiconv = extractor.getUrlIdHandler();
|
||||
|
||||
@@ -134,7 +134,7 @@ public class StreamInfo extends AbstractStreamInfo {
|
||||
streamInfo.audio_streams = new Vector<>();
|
||||
}
|
||||
//todo: make this quick and dirty solution a real fallback
|
||||
// same as the quick and dirty aboth
|
||||
// same as the quick and dirty above
|
||||
try {
|
||||
streamInfo.audio_streams.addAll(
|
||||
DashMpdParser.getAudioStreams(streamInfo.dashMpdUrl));
|
||||
@@ -173,9 +173,9 @@ public class StreamInfo extends AbstractStreamInfo {
|
||||
private static StreamInfo extractOptionalData(
|
||||
StreamInfo streamInfo, StreamExtractor extractor) {
|
||||
/* ---- optional data goes here: ---- */
|
||||
// If one of these failes, the frontend neets to handle that they are not available.
|
||||
// Exceptions are therfore not thrown into the frontend, but stored into the error List,
|
||||
// so the frontend can afterwads check where errors happend.
|
||||
// If one of these fails, the frontend needs to handle that they are not available.
|
||||
// Exceptions are therefore not thrown into the frontend, but stored into the error List,
|
||||
// so the frontend can afterwards check where errors happened.
|
||||
|
||||
try {
|
||||
streamInfo.thumbnail_url = extractor.getThumbnailUrl();
|
||||
@@ -290,4 +290,4 @@ public class StreamInfo extends AbstractStreamInfo {
|
||||
public int start_position = 0;
|
||||
|
||||
public List<Throwable> errors = new Vector<>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import android.media.AudioManager;
|
||||
import android.media.MediaPlayer;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.IBinder;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v7.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
@@ -27,6 +29,7 @@ import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created by Adam Howard on 08/11/15.
|
||||
@@ -51,10 +54,13 @@ import java.io.IOException;
|
||||
/**Plays the audio stream of videos in the background.*/
|
||||
public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPreparedListener*/ {
|
||||
|
||||
private static final String TAG = BackgroundPlayer.class.toString();
|
||||
private static final String ACTION_STOP = TAG + ".STOP";
|
||||
private static final String ACTION_PLAYPAUSE = TAG + ".PLAYPAUSE";
|
||||
private static final String ACTION_REWIND = TAG + ".REWIND";
|
||||
private static final String TAG = "BackgroundPlayer";
|
||||
private static final String CLASSNAME = "org.schabi.newpipe.player.BackgroundPlayer";
|
||||
private static final String ACTION_STOP = CLASSNAME + ".STOP";
|
||||
private static final String ACTION_PLAYPAUSE = CLASSNAME + ".PLAYPAUSE";
|
||||
private static final String ACTION_REWIND = CLASSNAME + ".REWIND";
|
||||
private static final String ACTION_PLAYBACK_STATE = CLASSNAME + ".PLAYBACK_STATE";
|
||||
private static final String EXTRA_PLAYBACK_STATE = CLASSNAME + ".extras.EXTRA_PLAYBACK_STATE";
|
||||
|
||||
// Extra intent arguments
|
||||
public static final String TITLE = "title";
|
||||
@@ -114,6 +120,7 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
|
||||
private class PlayerThread extends Thread {
|
||||
MediaPlayer mediaPlayer;
|
||||
private String source;
|
||||
@@ -123,8 +130,8 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
private NotificationManager noteMgr;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
private Bitmap videoThumbnail;
|
||||
private NotificationCompat.Builder noteBuilder;
|
||||
private Notification note;
|
||||
private NoteBuilder noteBuilder;
|
||||
private volatile boolean donePlaying = false;
|
||||
|
||||
public PlayerThread(String src, String title, BackgroundPlayer owner) {
|
||||
this.source = src;
|
||||
@@ -134,6 +141,45 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
||||
}
|
||||
|
||||
public boolean isDonePlaying() {
|
||||
return donePlaying;
|
||||
}
|
||||
|
||||
private boolean isPlaying() {
|
||||
try {
|
||||
return mediaPlayer.isPlaying();
|
||||
} catch (IllegalStateException e) {
|
||||
Log.w(TAG, "Unable to retrieve playing state", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDonePlaying() {
|
||||
donePlaying = true;
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized PlaybackState getPlaybackState() {
|
||||
try {
|
||||
return new PlaybackState(mediaPlayer.getDuration(), mediaPlayer.getCurrentPosition(), isPlaying());
|
||||
} catch (IllegalStateException e) {
|
||||
// This isn't that nice way to handle this.
|
||||
// maybe there is a better way
|
||||
Log.w(TAG, this + ": Got illegal state exception while creating playback state", e);
|
||||
return PlaybackState.UNPREPARED;
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastState() {
|
||||
PlaybackState state = getPlaybackState();
|
||||
if(state == null) return;
|
||||
Intent intent = new Intent(ACTION_PLAYBACK_STATE);
|
||||
intent.putExtra(EXTRA_PLAYBACK_STATE, state);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);//cpu lock
|
||||
@@ -181,27 +227,29 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
filter.addAction(ACTION_PLAYPAUSE);
|
||||
filter.addAction(ACTION_STOP);
|
||||
filter.addAction(ACTION_REWIND);
|
||||
filter.addAction(ACTION_PLAYBACK_STATE);
|
||||
registerReceiver(broadcastReceiver, filter);
|
||||
|
||||
note = buildNotification();
|
||||
|
||||
startForeground(noteID, note);
|
||||
initNotificationBuilder();
|
||||
startForeground(noteID, noteBuilder.build());
|
||||
|
||||
//currently decommissioned progressbar looping update code - works, but doesn't fit inside
|
||||
//Notification.MediaStyle Notification layout.
|
||||
noteMgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
|
||||
/*
|
||||
|
||||
//update every 2s or 4 times in the video, whichever is shorter
|
||||
int sleepTime = Math.min(2000, (int)((double)vidLength/4));
|
||||
while(mediaPlayer.isPlaying()) {
|
||||
noteBuilder.setProgress(vidLength, mediaPlayer.getCurrentPosition(), false);
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
int vidLength = mediaPlayer.getDuration();
|
||||
int sleepTime = Math.min(2000, (int)(vidLength / 4));
|
||||
while(!isDonePlaying()) {
|
||||
broadcastState();
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
synchronized (this) {
|
||||
wait(sleepTime);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "sleep failure");
|
||||
Log.e(TAG, "sleep failure", e);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
/**Handles button presses from the notification. */
|
||||
@@ -210,39 +258,50 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
//Log.i(TAG, "received broadcast action:"+action);
|
||||
if(action.equals(ACTION_PLAYPAUSE)) {
|
||||
if(mediaPlayer.isPlaying()) {
|
||||
mediaPlayer.pause();
|
||||
note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp);
|
||||
if(android.os.Build.VERSION.SDK_INT >=16){
|
||||
note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_play_circle_filled_white_24dp);
|
||||
switch (action) {
|
||||
case ACTION_PLAYPAUSE: {
|
||||
boolean isPlaying = mediaPlayer.isPlaying();
|
||||
if(isPlaying) {
|
||||
mediaPlayer.pause();
|
||||
} else {
|
||||
//reacquire CPU lock after auto-releasing it on pause
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mediaPlayer.start();
|
||||
}
|
||||
noteMgr.notify(noteID, note);
|
||||
}
|
||||
else {
|
||||
//reacquire CPU lock after auto-releasing it on pause
|
||||
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
|
||||
mediaPlayer.start();
|
||||
note.contentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp);
|
||||
if(android.os.Build.VERSION.SDK_INT >=16){
|
||||
note.bigContentView.setImageViewResource(R.id.notificationPlayPause, R.drawable.ic_pause_white_24dp);
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
noteMgr.notify(noteID, note);
|
||||
break;
|
||||
}
|
||||
case ACTION_REWIND:
|
||||
mediaPlayer.seekTo(0);
|
||||
synchronized (PlayerThread.this) {
|
||||
PlayerThread.this.notifyAll();
|
||||
}
|
||||
break;
|
||||
case ACTION_STOP:
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
break;
|
||||
case ACTION_PLAYBACK_STATE: {
|
||||
PlaybackState playbackState = intent.getParcelableExtra(EXTRA_PLAYBACK_STATE);
|
||||
if(!playbackState.equals(PlaybackState.UNPREPARED)) {
|
||||
noteBuilder.setProgress(playbackState.getDuration(), playbackState.getPlayedTime(), false);
|
||||
noteBuilder.setIsPlaying(playbackState.isPlaying());
|
||||
} else {
|
||||
noteBuilder.setProgress(0, 0, true);
|
||||
}
|
||||
noteMgr.notify(noteID, noteBuilder.build());
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if(action.equals(ACTION_REWIND)) {
|
||||
mediaPlayer.seekTo(0);
|
||||
// noteMgr.notify(noteID, note);
|
||||
}
|
||||
else if(action.equals(ACTION_STOP)) {
|
||||
//this auto-releases CPU lock
|
||||
mediaPlayer.stop();
|
||||
afterPlayCleanup();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private void afterPlayCleanup() {
|
||||
// Notify thread to stop
|
||||
setDonePlaying();
|
||||
//remove progress bar
|
||||
//noteBuilder.setProgress(0, 0, false);
|
||||
|
||||
@@ -256,7 +315,12 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
wifiLock.release();
|
||||
//remove foreground status of service; make BackgroundPlayer killable
|
||||
stopForeground(true);
|
||||
|
||||
try {
|
||||
// Wait for thread to stop
|
||||
PlayerThread.this.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "unable to join player thread", e);
|
||||
}
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@@ -272,10 +336,14 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
private void initNotificationBuilder() {
|
||||
Notification note;
|
||||
Resources res = getApplicationContext().getResources();
|
||||
noteBuilder = new NotificationCompat.Builder(owner);
|
||||
|
||||
/*
|
||||
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
|
||||
*/
|
||||
|
||||
PendingIntent playPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_PLAYPAUSE), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
@@ -283,10 +351,6 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
new Intent(ACTION_STOP), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
PendingIntent rewindPI = PendingIntent.getBroadcast(owner, noteID,
|
||||
new Intent(ACTION_REWIND), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
/*
|
||||
NotificationCompat.Action pauseButton = new NotificationCompat.Action.Builder
|
||||
(R.drawable.ic_pause_white_24dp, "Pause", playPI).build();
|
||||
*/
|
||||
|
||||
//build intent to return to video, on tapping notification
|
||||
Intent openDetailViewIntent = new Intent(getApplicationContext(),
|
||||
@@ -296,58 +360,226 @@ public class BackgroundPlayer extends Service /*implements MediaPlayer.OnPrepare
|
||||
openDetailViewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent openDetailView = PendingIntent.getActivity(owner, noteID,
|
||||
openDetailViewIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
noteBuilder = new NoteBuilder(owner, playPI, stopPI, rewindPI, openDetailView);
|
||||
noteBuilder
|
||||
.setTitle(title)
|
||||
.setArtist(channelName)
|
||||
.setOngoing(true)
|
||||
.setDeleteIntent(stopPI)
|
||||
//doesn't fit with Notification.MediaStyle
|
||||
//.setProgress(vidLength, 0, false)
|
||||
.setSmallIcon(R.drawable.ic_play_circle_filled_white_24dp)
|
||||
.setTicker(
|
||||
String.format(res.getString(
|
||||
R.string.background_player_time_text), title))
|
||||
.setContentIntent(PendingIntent.getActivity(getApplicationContext(),
|
||||
noteID, openDetailViewIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT))
|
||||
.setContentIntent(openDetailView);
|
||||
.setContentIntent(openDetailView)
|
||||
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||
//Make notification appear on lockscreen
|
||||
.setVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
|
||||
RemoteViews view =
|
||||
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
view.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
view.setTextViewText(R.id.notificationSongName, title);
|
||||
view.setTextViewText(R.id.notificationArtist, channelName);
|
||||
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
/**
|
||||
* Notification builder which works like the real builder but uses a custom view.
|
||||
*/
|
||||
class NoteBuilder extends NotificationCompat.Builder {
|
||||
|
||||
//possibly found the expandedView problem,
|
||||
//but can't test it as I don't have a 5.0 device. -medavox
|
||||
RemoteViews expandedView =
|
||||
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
|
||||
expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
expandedView.setTextViewText(R.id.notificationSongName, title);
|
||||
expandedView.setTextViewText(R.id.notificationArtist, channelName);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
|
||||
|
||||
noteBuilder.setCategory(Notification.CATEGORY_TRANSPORT);
|
||||
|
||||
//Make notification appear on lockscreen
|
||||
noteBuilder.setVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
|
||||
note = noteBuilder.build();
|
||||
note.contentView = view;
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT > 16) {
|
||||
note.bigContentView = expandedView;
|
||||
/**
|
||||
* @param context
|
||||
* @inheritDoc
|
||||
*/
|
||||
public NoteBuilder(Context context, PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI, PendingIntent openDetailView) {
|
||||
super(context);
|
||||
setCustomContentView(createCustomContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
setCustomBigContentView(createCustomBigContentView(playPI, stopPI, rewindPI, openDetailView));
|
||||
}
|
||||
|
||||
return note;
|
||||
private RemoteViews createCustomBigContentView(PendingIntent playPI,
|
||||
PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent openDetailView) {
|
||||
//possibly found the expandedView problem,
|
||||
//but can't test it as I don't have a 5.0 device. -medavox
|
||||
RemoteViews expandedView =
|
||||
new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification_expanded);
|
||||
expandedView.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
expandedView.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return expandedView;
|
||||
}
|
||||
|
||||
private RemoteViews createCustomContentView(PendingIntent playPI, PendingIntent stopPI,
|
||||
PendingIntent rewindPI,
|
||||
PendingIntent openDetailView) {
|
||||
RemoteViews view = new RemoteViews(BuildConfig.APPLICATION_ID, R.layout.player_notification);
|
||||
view.setImageViewBitmap(R.id.notificationCover, videoThumbnail);
|
||||
view.setOnClickPendingIntent(R.id.notificationStop, stopPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationPlayPause, playPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationRewind, rewindPI);
|
||||
view.setOnClickPendingIntent(R.id.notificationContent, openDetailView);
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of the stream
|
||||
* @param title the title of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setTitle(String title) {
|
||||
setContentTitle(title);
|
||||
getContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
getBigContentView().setTextViewText(R.id.notificationSongName, title);
|
||||
setTicker(String.format(getBaseContext().getString(
|
||||
R.string.background_player_time_text), title));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the artist of the stream
|
||||
* @param artist the artist of the stream
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
NoteBuilder setArtist(String artist) {
|
||||
setSubText(artist);
|
||||
getContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
getBigContentView().setTextViewText(R.id.notificationArtist, artist);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public android.support.v4.app.NotificationCompat.Builder setProgress(int max, int progress, boolean indeterminate) {
|
||||
super.setProgress(max, progress, indeterminate);
|
||||
getBigContentView().setProgressBar(R.id.playbackProgress, max, progress, indeterminate);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the isPlaying state
|
||||
* @param isPlaying the is playing state
|
||||
*/
|
||||
public void setIsPlaying(boolean isPlaying) {
|
||||
RemoteViews views = getContentView(), bigViews = getBigContentView();
|
||||
int imageSrc;
|
||||
if(isPlaying) {
|
||||
imageSrc = R.drawable.ic_pause_white_24dp;
|
||||
} else {
|
||||
imageSrc = R.drawable.ic_play_circle_filled_white_24dp;
|
||||
}
|
||||
views.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
bigViews.setImageViewResource(R.id.notificationPlayPause, imageSrc);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the state of the player.
|
||||
*/
|
||||
public static class PlaybackState implements Parcelable {
|
||||
|
||||
private static final int INDEX_IS_PLAYING = 0;
|
||||
private static final int INDEX_IS_PREPARED= 1;
|
||||
private static final int INDEX_HAS_ERROR = 2;
|
||||
private final int duration;
|
||||
private final int played;
|
||||
private final boolean[] booleanValues = new boolean[3];
|
||||
|
||||
static final PlaybackState UNPREPARED = new PlaybackState(false, false, false);
|
||||
static final PlaybackState FAILED = new PlaybackState(false, false, true);
|
||||
|
||||
|
||||
PlaybackState(Parcel in) {
|
||||
duration = in.readInt();
|
||||
played = in.readInt();
|
||||
in.readBooleanArray(booleanValues);
|
||||
}
|
||||
|
||||
PlaybackState(int duration, int played, boolean isPlaying) {
|
||||
this.played = played;
|
||||
this.duration = duration;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = true;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = false;
|
||||
}
|
||||
|
||||
private PlaybackState(boolean isPlaying, boolean isPrepared, boolean hasErrors) {
|
||||
this.played = 0;
|
||||
this.duration = 0;
|
||||
this.booleanValues[INDEX_IS_PLAYING] = isPlaying;
|
||||
this.booleanValues[INDEX_IS_PREPARED] = isPrepared;
|
||||
this.booleanValues[INDEX_HAS_ERROR] = hasErrors;
|
||||
}
|
||||
|
||||
int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
int getPlayedTime() {
|
||||
return played;
|
||||
}
|
||||
|
||||
boolean isPlaying() {
|
||||
return booleanValues[INDEX_IS_PLAYING];
|
||||
}
|
||||
|
||||
boolean isPrepared() {
|
||||
return booleanValues[INDEX_IS_PREPARED];
|
||||
}
|
||||
|
||||
boolean hasErrors() {
|
||||
return booleanValues[INDEX_HAS_ERROR];
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(duration);
|
||||
dest.writeInt(played);
|
||||
dest.writeBooleanArray(booleanValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<PlaybackState> CREATOR = new Creator<PlaybackState>() {
|
||||
@Override
|
||||
public PlaybackState createFromParcel(Parcel in) {
|
||||
return new PlaybackState(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PlaybackState[] newArray(int size) {
|
||||
return new PlaybackState[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PlaybackState that = (PlaybackState) o;
|
||||
|
||||
if (duration != that.duration) return false;
|
||||
if (played != that.played) return false;
|
||||
return Arrays.equals(booleanValues, that.booleanValues);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
if(this == UNPREPARED) return 1;
|
||||
if(this == FAILED) return 2;
|
||||
int result = duration;
|
||||
result = 31 * result + played;
|
||||
result = 31 * result + Arrays.hashCode(booleanValues);
|
||||
return result + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.support.v4.app.Fragment;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@@ -28,6 +29,9 @@ import org.schabi.newpipe.detail.VideoItemDetailActivity;
|
||||
import org.schabi.newpipe.detail.VideoItemDetailFragment;
|
||||
import org.schabi.newpipe.info_list.InfoListAdapter;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static org.schabi.newpipe.ReCaptchaActivity.RECAPTCHA_REQUEST;
|
||||
|
||||
/**
|
||||
* Created by Christian Schabesberger on 02.08.16.
|
||||
*
|
||||
@@ -68,7 +72,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
//noinspection ConstantConditions
|
||||
inputManager.hideSoftInputFromWindow(
|
||||
a.getCurrentFocus().getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
|
||||
} catch(NullPointerException e) {
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(a, e, null,
|
||||
a.findViewById(android.R.id.content),
|
||||
@@ -82,7 +86,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
// onQueryTextSubmit to trigger twice when focus is not cleared.
|
||||
// See: http://stackoverflow.com/questions/17874951/searchview-onquerytextsubmit-runs-twice-while-i-pressed-once
|
||||
a.getCurrentFocus().clearFocus();
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
View bg = a.findViewById(R.id.mainBG);
|
||||
@@ -92,7 +96,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
if(!newText.isEmpty()) {
|
||||
if (!newText.isEmpty()) {
|
||||
searchSuggestions(newText);
|
||||
}
|
||||
return true;
|
||||
@@ -133,13 +137,13 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if(savedInstanceState != null) {
|
||||
if (savedInstanceState != null) {
|
||||
searchQuery = savedInstanceState.getString(QUERY);
|
||||
streamingServiceId = savedInstanceState.getInt(STREAMING_SERVICE);
|
||||
} else {
|
||||
try {
|
||||
streamingServiceId = NewPipe.getIdOfService("Youtube");
|
||||
} catch(Exception e) {
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
ErrorActivity.reportError(getActivity(), e, null,
|
||||
getActivity().findViewById(android.R.id.content),
|
||||
@@ -180,9 +184,11 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
public void onReCaptchaChallenge() {
|
||||
Toast.makeText(getActivity(), "ReCaptcha Challenge requested",
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// Starting ReCaptcha Challenge Activity
|
||||
Intent i = new Intent(getActivity(), ReCaptchaActivity.class);
|
||||
getActivity().startActivity(i);
|
||||
startActivityForResult(
|
||||
new Intent(getActivity(), ReCaptchaActivity.class),
|
||||
RECAPTCHA_REQUEST);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -216,14 +222,13 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
int pastVisiblesItems, visibleItemCount, totalItemCount;
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
if(dy > 0) //check for scroll down
|
||||
if (dy > 0) //check for scroll down
|
||||
{
|
||||
visibleItemCount = streamInfoListLayoutManager.getChildCount();
|
||||
totalItemCount = streamInfoListLayoutManager.getItemCount();
|
||||
pastVisiblesItems = streamInfoListLayoutManager.findFirstVisibleItemPosition();
|
||||
|
||||
if ( (visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading)
|
||||
{
|
||||
if ((visibleItemCount + pastVisiblesItems) >= totalItemCount && !isLoading) {
|
||||
pageNumber++;
|
||||
search(searchQuery, pageNumber);
|
||||
}
|
||||
@@ -264,7 +269,7 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
searchView.setSuggestionsAdapter(suggestionListAdapter);
|
||||
searchView.setOnSuggestionListener(new SearchSuggestionListener(searchView, suggestionListAdapter));
|
||||
searchView.setOnQueryTextListener(new SearchQueryListener());
|
||||
if(searchQuery != null && !searchQuery.isEmpty()) {
|
||||
if (searchQuery != null && !searchQuery.isEmpty()) {
|
||||
searchView.setQuery(searchQuery, false);
|
||||
searchView.setIconifiedByDefault(false);
|
||||
}
|
||||
@@ -289,4 +294,23 @@ public class SearchInfoItemFragment extends Fragment {
|
||||
Thread suggestionThread = new Thread(suggestionSearchRunnable);
|
||||
suggestionThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case RECAPTCHA_REQUEST:
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (searchQuery.length() != 0) {
|
||||
search(searchQuery);
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "ReCaptcha failed");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(TAG, "Request code from activity not supported [" + requestCode + "]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
android:layout_above="@+id/notificationButtons"
|
||||
android:layout_toRightOf="@+id/notificationCover"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical" >
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notificationSongName"
|
||||
@@ -40,6 +40,13 @@
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:text="artist" />
|
||||
|
||||
<ProgressBar
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/playbackProgress"
|
||||
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
|
||||
android:layout_marginRight="8dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"><string name="main_bg_subtitle">Sentuh pencarian untuk memulai</string>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"><string name="main_bg_subtitle">Klik pada bagian search untuk memulai</string>
|
||||
<string name="view_count_text">%1$s tayang</string>
|
||||
<string name="upload_date_text">Diterbitkan pada %1$s</string>
|
||||
<string name="install">Pasang</string>
|
||||
@@ -288,4 +288,7 @@
|
||||
|
||||
|
||||
"</string>
|
||||
<string name="reCaptchaActivity">reCAPTCHA</string>
|
||||
<string name="reCaptcha_title">Rintangan reCAPTCHA</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -301,4 +301,7 @@
|
||||
|
||||
|
||||
"</string>
|
||||
<string name="reCaptchaActivity">reCaptcha</string>
|
||||
<string name="reCaptcha_title">Sfida reCaptcha</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -299,4 +299,7 @@
|
||||
|
||||
|
||||
"</string>
|
||||
<string name="reCaptchaActivity">reCaptcha</string>
|
||||
<string name="reCaptcha_title">reCaptcha の要求</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -292,4 +292,7 @@
|
||||
|
||||
|
||||
"</string>
|
||||
<string name="reCaptchaActivity">reCaptcha</string>
|
||||
<string name="reCaptcha_title">Výzva reCaptcha</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -150,4 +150,8 @@
|
||||
|
||||
<string name="could_not_load_image">无法加载图像</string>
|
||||
<string name="app_ui_crash">应用/界面已崩溃</string>
|
||||
<string name="info_labels">原因:\\n请求:\\n内容语言:\\n服务:\\nGMT时间:\\n包:\\n版本:\\n操作系统版本:\\nIP地理范围:</string>
|
||||
<string name="reCaptchaActivity">reCaptcha</string>
|
||||
<string name="reCaptcha_title">reCaptcha 验证</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="reCaptchaActivity">reCaptcha</string>
|
||||
<string name="reCaptcha_title">reCaptcha Challenge</string>
|
||||
<string name="recaptcha_request_toast">ReCaptcha Challenge requested</string>
|
||||
|
||||
<!-- End of GigaGet's Strings -->
|
||||
|
||||
|
||||
Reference in New Issue
Block a user