diff --git a/app/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java b/app/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java new file mode 100644 index 0000000..9b314f9 --- /dev/null +++ b/app/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java @@ -0,0 +1,25 @@ +package com.google.android.apps.youtube.app.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; + +public class SlimMetadataScrollableButtonContainerLayout extends ViewGroup { + + public SlimMetadataScrollableButtonContainerLayout(Context context) { + super(context); + } + + public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onLayout(boolean b, int i, int i1, int i2, int i3) { + + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java b/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java new file mode 100644 index 0000000..58dec2f --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ads/VideoAds.java @@ -0,0 +1,213 @@ +package fi.vanced.libraries.youtube.ads; + +import static fi.razerman.youtube.XGlobals.debug; +import static fi.vanced.libraries.youtube.player.VideoInformation.channelName; +import static fi.vanced.libraries.youtube.ui.SlimButtonContainer.adBlockButton; +import static fi.vanced.utils.VancedUtils.getPreferences; +import static fi.vanced.utils.VancedUtils.parseJson; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; + +import fi.razerman.youtube.XGlobals; +import fi.vanced.libraries.youtube.player.ChannelModel; +import fi.vanced.libraries.youtube.player.VideoInformation; +import fi.vanced.utils.ObjectSerializer; + +public class VideoAds { + public static final String TAG = "VI - VideoAds"; + public static final String PREFERENCES_NAME = "channel-whitelist"; + private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1"; + private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey"; + private static ArrayList whiteList; + private static Thread fetchThread = null; + + static { + whiteList = parseWhitelist(YouTubeTikTokRoot_Application.getAppContext()); + } + + // Call to this needs to be injected in YT code + public static void setChannelName(String channelName) { + if (debug) { + Log.d(TAG, "channel name set to " + channelName); + } + VideoInformation.channelName = channelName; + + if (adBlockButton != null) { + adBlockButton.changeEnabled(getShouldShowAds()); + } + } + + // Call to this needs to be injected in YT code (CURRENTLY NOT USED) + public static void newVideoLoaded(String videoId) { + if (debug) { + Log.d(TAG, "newVideoLoaded - " + videoId); + } + + try { + if (fetchThread != null && fetchThread.getState() != Thread.State.TERMINATED) { + if (debug) { + Log.d(TAG, "Interrupting the thread. Current state " + fetchThread.getState()); + } + fetchThread.interrupt(); + } + } + catch (Exception ex) { + Log.e(TAG, "Error in the fetch thread", ex); + } + + fetchThread = new Thread(() -> { + try { + if (debug) { + Log.d(TAG, "Fetching channelId for " + videoId); + } + HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json; utf-8"); + connection.setRequestProperty("Accept", "application/json"); + connection.setDoOutput(true); + connection.setConnectTimeout(2 * 1000); + + // TODO: Actually fetch the version + String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + videoId + "\"}"; + try(OutputStream os = connection.getOutputStream()) { + byte[] input = jsonInputString.getBytes("utf-8"); + os.write(input, 0, input.length); + } + if (connection.getResponseCode() == 200) { + JSONObject json = new JSONObject(parseJson(connection)); + JSONObject videoInfo = json.getJSONObject("videoDetails"); + ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId")); + if (debug) { + Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor()); + } + } + else if (debug) { + Log.d(TAG, "player fetch response was " + connection.getResponseCode()); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to fetch channelId", ex); + return; + } + }); + + fetchThread.start(); + } + + public static boolean getShouldShowAds() { + if (channelName == null || channelName.isEmpty() || channelName.trim().isEmpty()) { + if (XGlobals.debug) { + Log.d(TAG, "getShouldShowAds skipped because channelId was null"); + } + + return false; + } + + for (ChannelModel channelModel: whiteList) { + if (channelModel.getAuthor().equals(channelName)) { + if (XGlobals.debug) { + Log.d(TAG, "Video ad whitelist for " + channelName); + } + + return true; + } + } + + return false; + } + + public static boolean addToWhitelist(Context context, String channelName, String channelId) { + try { + whiteList.add(new ChannelModel(channelName, channelId)); + updateWhitelist(context); + return true; + } + catch (Exception ex) { + Log.d(TAG, "Unable to add " + channelName + " with id " + channelId + " to whitelist"); + } + + return false; + } + + public static boolean removeFromWhitelist(Context context, String channelName) { + try { + //whiteList.removeIf(x -> x.getAuthor().equals(channelName)); // Requires Android N + + Iterator iterator = whiteList.iterator(); + while(iterator.hasNext()) + { + ChannelModel value = iterator.next(); + if (value.getAuthor().equals(channelName)) + { + iterator.remove(); + break; + } + } + updateWhitelist(context); + return true; + } + catch (Exception ex) { + Log.d(TAG, "Unable to remove " + channelName + " from whitelist"); + } + + return false; + } + + private static void updateWhitelist(Context context) { + if (context == null) return; + + SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); + SharedPreferences.Editor editor = preferences.edit(); + + try { + editor.putString("channels", ObjectSerializer.serialize(whiteList)); + } catch (IOException e) { + e.printStackTrace(); + } + + editor.apply(); + } + + private static ArrayList parseWhitelist(Context context) { + if (context == null) return new ArrayList<>(); + + SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); + try { + String channels = preferences.getString("channels", null); + if (channels == null) { + if (debug) { + Log.d(TAG, "channels string was null for ad whitelisting"); + } + + return new ArrayList<>(); + } + + ArrayList channelModels = (ArrayList) ObjectSerializer.deserialize(channels); + if (debug) { + Log.d(TAG, channels); + for (ChannelModel channelModel: channelModels) { + Log.d(TAG, "Ad whitelisted " + channelModel.getAuthor() + " with id of " + channelModel.getChannelId()); + } + } + + return channelModels; + } catch (IOException e) { + e.printStackTrace(); + } + + return new ArrayList<>(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/player/ChannelModel.java b/app/src/main/java/fi/vanced/libraries/youtube/player/ChannelModel.java new file mode 100644 index 0000000..3ad86fe --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/player/ChannelModel.java @@ -0,0 +1,29 @@ +package fi.vanced.libraries.youtube.player; + +import java.io.Serializable; + +public class ChannelModel implements Serializable { + private String author; + private String channelId; + + public ChannelModel(String author, String channelId) { + this.author = author; + this.channelId = channelId; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getChannelId() { + return channelId; + } + + public void setChannelId(String channelId) { + this.channelId = channelId; + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/player/VideoHelpers.java b/app/src/main/java/fi/vanced/libraries/youtube/player/VideoHelpers.java new file mode 100644 index 0000000..93e921f --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/player/VideoHelpers.java @@ -0,0 +1,62 @@ +package fi.vanced.libraries.youtube.player; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import static fi.razerman.youtube.XGlobals.debug; +import static pl.jakubweg.StringRef.str; + +public class VideoHelpers { + public static final String TAG = "VideoHelpers"; + + public static void copyVideoUrlToClipboard() { + generateVideoUrl(false); + } + + public static void copyVideoUrlWithTimeStampToClipboard() { + generateVideoUrl(true); + } + + private static void generateVideoUrl(boolean appendTimeStamp) { + try { + String videoId = VideoInformation.currentVideoId; + if (videoId == null || videoId.isEmpty()) { + if (debug) { + Log.d(TAG, "VideoId was empty"); + } + return; + } + + String videoUrl = String.format("https://youtu.be/%s", videoId); + if (appendTimeStamp) { + long videoTime = VideoInformation.lastKnownVideoTime; + videoUrl += String.format("?t=%s", (videoTime / 1000)); + } + + if (debug) { + Log.d(TAG, "Video URL: " + videoUrl); + } + + setClipboard(YouTubeTikTokRoot_Application.getAppContext(), videoUrl); + + Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), str("share_copy_url_success"), Toast.LENGTH_SHORT).show(); + } + catch (Exception ex) { + Log.e(TAG, "Couldn't generate video url", ex); + } + } + + private static void setClipboard(Context context, String text) { + if(android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) { + android.text.ClipboardManager clipboard = (android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + clipboard.setText(text); + } else { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("link", text); + clipboard.setPrimaryClip(clip); + } + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/player/VideoInformation.java b/app/src/main/java/fi/vanced/libraries/youtube/player/VideoInformation.java index a741118..968055b 100644 --- a/app/src/main/java/fi/vanced/libraries/youtube/player/VideoInformation.java +++ b/app/src/main/java/fi/vanced/libraries/youtube/player/VideoInformation.java @@ -1,6 +1,45 @@ package fi.vanced.libraries.youtube.player; +import static fi.razerman.youtube.XGlobals.debug; + +import android.util.Log; + +import fi.vanced.libraries.youtube.ryd.ReturnYouTubeDislikes; + public class VideoInformation { + private static final String TAG = "VI - VideoInfo"; + public static String currentVideoId; + public static Integer dislikeCount = null; + public static String channelName = null; public static long lastKnownVideoTime = -1L; + + // Call hook in the YT code when the video changes + public static void setCurrentVideoId(final String videoId) { + if (videoId == null) { + if (debug) { + Log.d(TAG, "setCurrentVideoId - new id was null - currentVideoId was" + currentVideoId); + } + currentVideoId = null; + dislikeCount = null; + channelName = null; + return; + } + + if (videoId.equals(currentVideoId)) { + if (debug) { + Log.d(TAG, "setCurrentVideoId - new and current video were equal - " + videoId); + } + return; + } + + if (debug) { + Log.d(TAG, "setCurrentVideoId - video id updated from " + currentVideoId + " to " + videoId); + } + + currentVideoId = videoId; + + // New video + ReturnYouTubeDislikes.newVideoLoaded(videoId); + } } diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java new file mode 100644 index 0000000..263e649 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Registration.java @@ -0,0 +1,149 @@ +package fi.vanced.libraries.youtube.ryd; + +import static fi.razerman.youtube.XGlobals.debug; +import static fi.vanced.utils.VancedUtils.getPreferences; +import static fi.vanced.utils.VancedUtils.parseJson; +import static fi.vanced.utils.VancedUtils.randomString; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class Registration { + private static final String TAG = "VI - RYD - Registration"; + public static final String PREFERENCES_NAME = "ryd"; + + private String userId; + private Context context; + + public Registration(Context context) { + this.context = context; + } + + public String getUserId() { + return userId != null ? userId : fetchUserId(); + } + + private String fetchUserId() { + try { + if (this.context == null) throw new Exception("Unable to fetch userId because context was null"); + + SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); + this.userId = preferences.getString("userId", null); + + if (this.userId == null) { + this.userId = register(); + } + } + catch (Exception ex) { + Log.e(TAG, "Unable to fetch the userId from shared preferences", ex); + } + + return this.userId; + } + + private void saveUserId(String userId) { + try { + if (this.context == null) throw new Exception("Unable to save userId because context was null"); + + SharedPreferences preferences = getPreferences(context, PREFERENCES_NAME); + SharedPreferences.Editor editor = preferences.edit(); + editor.putString("userId", userId).apply(); + } + catch (Exception ex) { + Log.e(TAG, "Unable to save the userId in shared preferences", ex); + } + } + + private String register() { + try { + // Generate a new userId + String userId = randomString(36); + if (debug) { + Log.d(TAG, "Trying to register the following userId: " + userId); + } + + // Get the registration challenge + HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection(); + connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + connection.setConnectTimeout(5 * 1000); + if (connection.getResponseCode() == 200) { + JSONObject json = new JSONObject(parseJson(connection)); + String challenge = json.getString("challenge"); + int difficulty = json.getInt("difficulty"); + if (debug) { + Log.d(TAG, "Registration challenge - " + challenge + " with difficulty of " + difficulty); + } + + // Solve the puzzle + String solution = Utils.solvePuzzle(challenge, difficulty); + if (debug) { + Log.d(TAG, "Registration confirmation solution is " + solution); + } + + return confirmRegistration(userId, solution); + } + else if (debug) { + Log.d(TAG, "Registration response was " + connection.getResponseCode()); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to register userId", ex); + } + + return null; + } + + public String confirmRegistration(String userId, String solution) { + try { + if (debug) { + Log.d(TAG, "Trying to confirm registration for the following userId: " + userId + " with solution: " + solution); + } + + // Confirm registration + HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/puzzle/registration?userId=" + userId).openConnection(); + confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + confirmationCon.setRequestMethod("POST"); + confirmationCon.setRequestProperty("Content-Type", "application/json"); + confirmationCon.setRequestProperty("Accept", "application/json"); + confirmationCon.setDoOutput(true); + confirmationCon.setConnectTimeout(5 * 1000); + + String jsonInputString = "{\"solution\": \"" + solution + "\"}"; + try(OutputStream os = confirmationCon.getOutputStream()) { + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + if (confirmationCon.getResponseCode() == 200) { + String result = parseJson(confirmationCon); + if (debug) { + Log.d(TAG, "Registration confirmation result was " + result); + } + + if (result.equalsIgnoreCase("true")) { + saveUserId(userId); + if (debug) { + Log.d(TAG, "Registration was successful for user " + userId); + } + + return userId; + } + } + else if (debug) { + Log.d(TAG, "Registration confirmation response was " + confirmationCon.getResponseCode()); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to confirm registration", ex); + } + + return null; + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java new file mode 100644 index 0000000..79d5a48 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/ReturnYouTubeDislikes.java @@ -0,0 +1,285 @@ +package fi.vanced.libraries.youtube.ryd; + +import static fi.razerman.youtube.XGlobals.debug; +import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId; +import static fi.vanced.utils.VancedUtils.getIdentifier; +import static fi.vanced.utils.VancedUtils.parseJson; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.URL; + +import static fi.vanced.libraries.youtube.player.VideoInformation.dislikeCount; + +public class ReturnYouTubeDislikes { + public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com"; + private static final String TAG = "VI - RYD"; + private static View _dislikeView = null; + private static Thread _dislikeFetchThread = null; + private static Thread _votingThread = null; + private static Registration registration; + private static Voting voting; + private static boolean likeActive; + private static boolean dislikeActive; + private static int votingValue = 0; // 1 = like, -1 = dislike, 0 = no vote + + static { + registration = new Registration(YouTubeTikTokRoot_Application.getAppContext()); + voting = new Voting(YouTubeTikTokRoot_Application.getAppContext(), registration); + } + + public static void newVideoLoaded(String videoId) { + if (debug) { + Log.d(TAG, "newVideoLoaded - " + videoId); + } + + try { + if (_dislikeFetchThread != null && _dislikeFetchThread.getState() != Thread.State.TERMINATED) { + if (debug) { + Log.d(TAG, "Interrupting the thread. Current state " + _dislikeFetchThread.getState()); + } + _dislikeFetchThread.interrupt(); + } + } + catch (Exception ex) { + Log.e(TAG, "Error in the dislike fetch thread", ex); + } + + _dislikeFetchThread = new Thread(() -> { + try { + if (debug) { + Log.d(TAG, "Fetching dislikes for " + videoId); + } + HttpURLConnection connection = (HttpURLConnection) new URL(RYD_API_URL + "/votes?videoId=" + videoId).openConnection(); + connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + connection.setConnectTimeout(5 * 1000); + if (connection.getResponseCode() == 200) { + JSONObject json = new JSONObject(parseJson(connection)); + dislikeCount = json.getInt("dislikes"); + if (debug) { + Log.d(TAG, "dislikes fetched - " + dislikeCount); + } + + // Set the dislikes + new Handler(Looper.getMainLooper()).post(new Runnable () { + @Override + public void run () { + trySetDislikes(String.valueOf(dislikeCount)); + } + }); + } + else if (debug) { + Log.d(TAG, "dislikes fetch response was " + connection.getResponseCode()); + } + } + catch (Exception ex) { + dislikeCount = null; + Log.e(TAG, "Failed to fetch dislikes", ex); + return; + } + }); + _dislikeFetchThread.start(); + } + + // Call to this needs to be injected in YT code + public static void setLikeTag(View view) { + setTag(view, "like"); + } + + public static void setLikeTag(View view, boolean active) { + likeActive = active; + if (likeActive) { + votingValue = 1; + } + if (debug) { + Log.d(TAG, "Like tag active " + likeActive); + } + setTag(view, "like"); + } + + // Call to this needs to be injected in YT code + public static void setDislikeTag(View view) { + _dislikeView = view; + setTag(view, "dislike"); + } + + public static void setDislikeTag(View view, boolean active) { + dislikeActive = active; + if (dislikeActive) { + votingValue = -1; + } + _dislikeView = view; + if (debug) { + Log.d(TAG, "Dislike tag active " + dislikeActive); + } + setTag(view, "dislike"); + } + + // Call to this needs to be injected in YT code + public static CharSequence onSetText(View view, CharSequence originalText) { + return handleOnSetText(view, originalText); + } + + // Call to this needs to be injected in YT code + public static void onClick(View view, boolean inactive) { + handleOnClick(view, inactive); + } + + private static CharSequence handleOnSetText(View view, CharSequence originalText) { + try { + CharSequence tag = (CharSequence) view.getTag(); + if (debug) { + Log.d(TAG, "handleOnSetText - " + tag + " - original text - " + originalText); + } + if (tag == null) return originalText; + + if (tag == "like") { + return originalText; + } + else if (tag == "dislike") { + return dislikeCount != null ? String.valueOf(dislikeCount) : originalText; + } + } + catch (Exception ex) { + Log.e(TAG, "Error while handling the setText", ex); + } + + return originalText; + } + + private static void trySetDislikes(String dislikeCount) { + try { + // Try to set normal video dislike count + if (_dislikeView == null) { + if (debug) { Log.d(TAG, "_dislikeView was null"); } + return; + } + + View buttonView = _dislikeView.findViewById(getIdentifier("button_text", "id")); + if (buttonView == null) { + if (debug) { Log.d(TAG, "buttonView was null"); } + return; + } + TextView button = (TextView) buttonView; + button.setText(dislikeCount); + if (debug) { + Log.d(TAG, "trySetDislikes - " + dislikeCount); + } + } + catch (Exception ex) { + if (debug) { + Log.e(TAG, "Error while trying to set dislikes text", ex); + } + } + } + + private static void handleOnClick(View view, boolean previousState) { + try { + String tag = (String) view.getTag(); + if (debug) { + Log.d(TAG, "handleOnClick - " + tag + " - previousState - " + previousState); + } + if (tag == null) return; + + // If active status was removed, vote should be none + if (previousState) { votingValue = 0; } + if (tag == "like") { + dislikeActive = false; + + // Like was activated + if (!previousState) { votingValue = 1; likeActive = true; } + else { likeActive = false; } + + // Like was activated and dislike was previously activated + if (!previousState && dislikeActive) { dislikeCount--; trySetDislikes(String.valueOf(dislikeCount)); } + } + else if (tag == "dislike") { + likeActive = false; + + // Dislike was activated + if (!previousState) { votingValue = -1; dislikeActive = true; dislikeCount++; } + // Dislike was removed + else { dislikeActive = false; dislikeCount--; } + trySetDislikes(String.valueOf(dislikeCount)); + } + else { + // Unknown tag + return; + } + + if (debug) { + Log.d(TAG, "New vote status - " + votingValue); + Log.d(TAG, "Like button " + likeActive + " | Dislike button " + dislikeActive); + } + + Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Voting value: " + votingValue, Toast.LENGTH_SHORT).show(); + + sendVote(votingValue); + } + catch (Exception ex) { + Log.e(TAG, "Error while handling the onClick", ex); + } + } + + private static void sendVote(int vote) { + if (debug) { + Log.d(TAG, "sending vote - " + vote + " for video " + currentVideoId); + } + + try { + if (_votingThread != null && _votingThread.getState() != Thread.State.TERMINATED) { + if (debug) { + Log.d(TAG, "Interrupting the thread. Current state " + _votingThread.getState()); + } + _votingThread.interrupt(); + } + } + catch (Exception ex) { + Log.e(TAG, "Error in the voting thread", ex); + } + + _votingThread = new Thread(() -> { + try { + boolean result = voting.sendVote(currentVideoId, vote); + if (debug) { + Log.d(TAG, "sendVote status " + result); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to send vote", ex); + return; + } + }); + _votingThread.start(); + } + + private static void setTag(View view, String tag) { + try { + if (view == null) { + if (debug) { + Log.d(TAG, "View was empty"); + } + return; + } + + if (debug) { + Log.d(TAG, "setTag - " + tag); + } + + view.setTag(tag); + } + catch (Exception ex) { + Log.e(TAG, "Error while trying to set tag to view", ex); + } + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java new file mode 100644 index 0000000..9ee92c2 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Utils.java @@ -0,0 +1,64 @@ +package fi.vanced.libraries.youtube.ryd; + +import android.util.Log; +import android.util.Base64; +import java.security.MessageDigest; + +public class Utils { + private static final String TAG = "VI - RYD - Utils"; + + static String solvePuzzle(String challenge, int difficulty) { + byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP); + + byte[] buffer = new byte[20]; + for (int i = 4; i < 20; i++) { + buffer[i] = decodedChallenge[i - 4]; + } + + try { + int maxCount = (int) (Math.pow(2, difficulty + 1) * 5); + MessageDigest md = MessageDigest.getInstance("SHA-512"); + for (int i = 0; i < maxCount; i++) { + buffer[0] = (byte)i; + buffer[1] = (byte)(i >> 8); + buffer[2] = (byte)(i >> 16); + buffer[3] = (byte)(i >> 24); + byte[] messageDigest = md.digest(buffer); + + if (countLeadingZeroes(messageDigest) >= difficulty) { + String encode = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP); + return encode; + } + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to solve puzzle", ex); + } + + return null; + } + + static int countLeadingZeroes(byte[] uInt8View) { + int zeroes = 0; + int value = 0; + for (int i = 0; i < uInt8View.length; i++) { + value = uInt8View[i] & 0xFF; + if (value == 0) { + zeroes += 8; + } else { + int count = 1; + if (value >>> 4 == 0) { + count += 4; + value <<= 4; + } + if (value >>> 6 == 0) { + count += 2; + value <<= 2; + } + zeroes += count - (value >>> 7); + break; + } + } + return zeroes; + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java new file mode 100644 index 0000000..fd53bf5 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ryd/Voting.java @@ -0,0 +1,120 @@ +package fi.vanced.libraries.youtube.ryd; + +import static fi.razerman.youtube.XGlobals.debug; +import static fi.vanced.utils.VancedUtils.parseJson; + +import android.content.Context; +import android.util.Log; + +import org.json.JSONObject; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class Voting { + private static final String TAG = "VI - RYD - Voting"; + + private Registration registration; + private Context context; + + public Voting(Context context, Registration registration) { + this.context = context; + this.registration = registration; + } + + public boolean sendVote(String videoId, int vote) { + try { + String userId = registration.getUserId(); + if (debug) { + Log.d(TAG, "Trying to vote the following video: " + videoId + " with vote " + vote + " and userId: " + userId); + } + + // Send the vote + HttpURLConnection connection = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/vote").openConnection(); + connection.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setDoOutput(true); + connection.setConnectTimeout(5 * 1000); + String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote + "\"}"; + try(OutputStream os = connection.getOutputStream()) { + byte[] input = voteJsonString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + if (connection.getResponseCode() == 200) { + JSONObject json = new JSONObject(parseJson(connection)); + String challenge = json.getString("challenge"); + int difficulty = json.getInt("difficulty"); + if (debug) { + Log.d(TAG, "Vote challenge - " + challenge + " with difficulty of " + difficulty); + } + + // Solve the puzzle + String solution = Utils.solvePuzzle(challenge, difficulty); + if (debug) { + Log.d(TAG, "Vote confirmation solution is " + solution); + } + + // Confirm vote + return confirmVote(userId, videoId, solution); + } + else if (debug) { + Log.d(TAG, "Vote response was " + connection.getResponseCode()); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to send vote", ex); + } + + return false; + } + + public boolean confirmVote(String userId, String videoId, String solution) { + try { + if (debug) { + Log.d(TAG, "Trying to confirm vote for video: " + videoId + " with solution " + solution + " and userId: " + userId); + } + + // Confirm vote + HttpURLConnection confirmationCon = (HttpURLConnection) new URL(ReturnYouTubeDislikes.RYD_API_URL + "/interact/confirmVote").openConnection(); + confirmationCon.setRequestProperty("User-agent", System.getProperty("http.agent") + ";vanced"); + confirmationCon.setRequestMethod("POST"); + confirmationCon.setRequestProperty("Content-Type", "application/json"); + confirmationCon.setRequestProperty("Accept", "application/json"); + confirmationCon.setDoOutput(true); + confirmationCon.setConnectTimeout(5 * 1000); + + String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; + try(OutputStream os = confirmationCon.getOutputStream()) { + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + if (confirmationCon.getResponseCode() == 200) { + String result = parseJson(confirmationCon); + if (debug) { + Log.d(TAG, "Vote confirmation result was " + result); + } + + if (result.equalsIgnoreCase("true")) { + if (debug) { + Log.d(TAG, "Vote was successful for user " + userId); + } + + return true; + } + } + else if (debug) { + Log.d(TAG, "Vote confirmation response was " + confirmationCon.getResponseCode()); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to send vote", ex); + } + + return false; + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java new file mode 100644 index 0000000..58700fb --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/AdBlock.java @@ -0,0 +1,135 @@ +package fi.vanced.libraries.youtube.ui; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.Toast; + +import static fi.razerman.youtube.XGlobals.debug; +import static fi.vanced.libraries.youtube.ads.VideoAds.getShouldShowAds; +import static fi.vanced.libraries.youtube.player.VideoInformation.currentVideoId; +import static fi.vanced.utils.VancedUtils.parseJson; +import static pl.jakubweg.StringRef.str; + +import org.json.JSONObject; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import fi.vanced.libraries.youtube.ads.VideoAds; +import fi.vanced.libraries.youtube.player.ChannelModel; +import fi.vanced.libraries.youtube.player.VideoInformation; +import fi.vanced.utils.VancedUtils; + +public class AdBlock extends SlimButton { + private static final String TAG = "VI - AdBlock - Button"; + private static final String YT_API_URL = "https://www.youtube.com/youtubei/v1"; + private static final String YT_API_KEY = "replaceMeWithTheYouTubeAPIKey"; + + public AdBlock(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, true); + + initialize(); + } + + private void initialize() { + this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_ad_button", "drawable")); + this.button_text.setText(str("action_ads")); + changeEnabled(getShouldShowAds()); + } + + public void changeEnabled(boolean enabled) { + if (debug) { + Log.d(TAG, "changeEnabled " + enabled); + } + this.button_icon.setEnabled(enabled); + } + + @Override + public void onClick(View view) { + this.view.setEnabled(false); + if (this.button_icon.isEnabled()) { + removeFromWhitelist(); + return; + } + //this.button_icon.setEnabled(!this.button_icon.isEnabled()); + + addToWhiteList(this.view, this.button_icon); + } + + private void removeFromWhitelist() { + try { + VideoAds.removeFromWhitelist(this.context, VideoInformation.channelName); + this.button_icon.setEnabled(false); + } + catch (Exception ex) { + Log.e(TAG, "Failed to remove from whitelist", ex); + return; + } + + this.view.setEnabled(true); + } + + private void addToWhiteList(View view, ImageView buttonIcon) { + new Thread(() -> { + try { + if (debug) { + Log.d(TAG, "Fetching channelId for " + currentVideoId); + } + HttpURLConnection connection = (HttpURLConnection) new URL(YT_API_URL + "/player?key=" + YT_API_KEY).openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json; utf-8"); + connection.setRequestProperty("Accept", "application/json"); + connection.setDoOutput(true); + connection.setConnectTimeout(2 * 1000); + + // TODO: Actually fetch the version + String jsonInputString = "{\"context\": {\"client\": { \"clientName\": \"Android\", \"clientVersion\": \"16.49.37\" } }, \"videoId\": \"" + currentVideoId + "\"}"; + try(OutputStream os = connection.getOutputStream()) { + byte[] input = jsonInputString.getBytes("utf-8"); + os.write(input, 0, input.length); + } + if (connection.getResponseCode() == 200) { + JSONObject json = new JSONObject(parseJson(connection)); + JSONObject videoInfo = json.getJSONObject("videoDetails"); + ChannelModel channelModel = new ChannelModel(videoInfo.getString("author"), videoInfo.getString("channelId")); + if (debug) { + Log.d(TAG, "channelId " + channelModel.getChannelId() + " fetched for author " + channelModel.getAuthor()); + } + + boolean success = VideoAds.addToWhitelist(this.context, channelModel.getAuthor(), channelModel.getChannelId()); + new Handler(Looper.getMainLooper()).post(() -> { + if (success) { + buttonIcon.setEnabled(true); + Toast.makeText(context, "Channel " + channelModel.getAuthor() + " whitelisted", Toast.LENGTH_SHORT).show(); + } + else { + buttonIcon.setEnabled(false); + Toast.makeText(context, "Channel " + channelModel.getAuthor() + " failed to whitelist", Toast.LENGTH_SHORT).show(); + } + + view.setEnabled(true); + }); + } + else { + if (debug) { + Log.d(TAG, "player fetch response was " + connection.getResponseCode()); + } + + buttonIcon.setEnabled(false); + this.view.setEnabled(true); + } + } + catch (Exception ex) { + Log.e(TAG, "Failed to fetch channelId", ex); + this.view.setEnabled(true); + return; + } + }).start(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyButton.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyButton.java new file mode 100644 index 0000000..00e45e7 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyButton.java @@ -0,0 +1,29 @@ +package fi.vanced.libraries.youtube.ui; + +import static pl.jakubweg.StringRef.str; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import fi.vanced.libraries.youtube.player.VideoHelpers; +import fi.vanced.utils.SharedPrefUtils; +import fi.vanced.utils.VancedUtils; + +public class CopyButton extends SlimButton { + public CopyButton(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, SharedPrefUtils.getBoolean(context, "youtube", "pref_copy_video_url_button", false)); + + initialize(); + } + + private void initialize() { + this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon", "drawable")); + this.button_text.setText(str("action_copy")); + } + + @Override + public void onClick(View view) { + VideoHelpers.copyVideoUrlToClipboard(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyWithTimestamp.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyWithTimestamp.java new file mode 100644 index 0000000..1a05dab --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/CopyWithTimestamp.java @@ -0,0 +1,29 @@ +package fi.vanced.libraries.youtube.ui; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import fi.vanced.libraries.youtube.player.VideoHelpers; +import fi.vanced.utils.SharedPrefUtils; +import fi.vanced.utils.VancedUtils; + +import static pl.jakubweg.StringRef.str; + +public class CopyWithTimestamp extends SlimButton { + public CopyWithTimestamp(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, SharedPrefUtils.getBoolean(context, "youtube", "pref_copy_video_url_timestamp_button", false)); + + initialize(); + } + + private void initialize() { + this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_yt_copy_icon_with_time", "drawable")); + this.button_text.setText(str("action_tcopy")); + } + + @Override + public void onClick(View view) { + VideoHelpers.copyVideoUrlWithTimeStampToClipboard(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButton.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButton.java new file mode 100644 index 0000000..41164ff --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButton.java @@ -0,0 +1,61 @@ +package fi.vanced.libraries.youtube.ui; + +import static fi.razerman.youtube.XGlobals.debug; + +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import fi.vanced.utils.VancedUtils; + +public abstract class SlimButton implements View.OnClickListener { + private static final String TAG = "VI - Slim - Button"; + public static int SLIM_METADATA_BUTTON_ID; + public final View view; + public final Context context; + private final ViewGroup container; + protected final ImageView button_icon; + protected final TextView button_text; + + static { + SLIM_METADATA_BUTTON_ID = VancedUtils.getIdentifier("slim_metadata_button", "layout"); + } + + public SlimButton(Context context, ViewGroup container, int id, boolean visible) { + if (debug) { + Log.d(TAG, "Adding button with id " + id + " and visibility of " + visible); + } + this.context = context; + this.container = container; + view = LayoutInflater.from(context).inflate(id, container, false); + button_icon = (ImageView)view.findViewById(VancedUtils.getIdentifier("button_icon", "id")); + button_text = (TextView)view.findViewById(VancedUtils.getIdentifier("button_text", "id")); + + view.setOnClickListener(this); + setVisible(visible); + + container.addView(view); + } + + public void setVisible(boolean visible) { + view.setVisibility(visible ? View.VISIBLE : View.GONE); + setContainerVisibility(); + } + + private void setContainerVisibility() { + if (container == null) return; + + for (int i = 0; i < container.getChildCount(); i++) { + if (container.getChildAt(i).getVisibility() == View.VISIBLE) { + container.setVisibility(View.VISIBLE); + return; + } + } + + container.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java new file mode 100644 index 0000000..a316c3b --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SlimButtonContainer.java @@ -0,0 +1,49 @@ +package fi.vanced.libraries.youtube.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ViewGroup; + +import com.google.android.apps.youtube.app.ui.SlimMetadataScrollableButtonContainerLayout; + +import fi.vanced.utils.VancedUtils; + +public class SlimButtonContainer extends SlimMetadataScrollableButtonContainerLayout { + private static final String TAG = "VI - Slim - Container"; + private ViewGroup container; + private CopyButton copyButton; + private CopyWithTimestamp copyWithTimestampButton; + public static AdBlock adBlockButton; + + public SlimButtonContainer(Context context) { + super(context); + this.initialize(context); + } + + public SlimButtonContainer(Context context, AttributeSet attrs) { + super(context, attrs); + this.initialize(context); + } + + public SlimButtonContainer(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.initialize(context); + } + + public void initialize(Context context) { + try { + container = this.findViewById(VancedUtils.getIdentifier("button_container_vanced", "id")); + if (container == null) throw new Exception("Unable to initialize the button container because the button_container_vanced couldn't be found"); + + copyButton = new CopyButton(context, this); + copyWithTimestampButton = new CopyWithTimestamp(context, this); + adBlockButton = new AdBlock(context, this); + new SponsorBlock(context, this); + new SponsorBlockVoting(context, this); + } + catch (Exception ex) { + Log.e(TAG, "Unable to initialize the button container", ex); + } + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java new file mode 100644 index 0000000..47132d5 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlock.java @@ -0,0 +1,31 @@ +package fi.vanced.libraries.youtube.ui; + +import static pl.jakubweg.StringRef.str; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import fi.vanced.libraries.youtube.player.VideoHelpers; +import fi.vanced.utils.VancedUtils; + +public class SponsorBlock extends SlimButton { + public SponsorBlock(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false); + + initialize(); + } + + private void initialize() { + this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_sb_logo", "drawable")); + this.button_text.setText("SB"); + } + + @Override + public void onClick(View view) { + Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlockVoting.java b/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlockVoting.java new file mode 100644 index 0000000..283b650 --- /dev/null +++ b/app/src/main/java/fi/vanced/libraries/youtube/ui/SponsorBlockVoting.java @@ -0,0 +1,28 @@ +package fi.vanced.libraries.youtube.ui; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import fi.vanced.utils.VancedUtils; + +public class SponsorBlockVoting extends SlimButton { + public SponsorBlockVoting(Context context, ViewGroup container) { + super(context, container, SlimButton.SLIM_METADATA_BUTTON_ID, false); + + initialize(); + } + + private void initialize() { + this.button_icon.setImageResource(VancedUtils.getIdentifier("vanced_sb_voting", "drawable")); + this.button_text.setText("SB Voting"); + } + + @Override + public void onClick(View view) { + Toast.makeText(YouTubeTikTokRoot_Application.getAppContext(), "Nothing atm", Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/fi/vanced/utils/ObjectSerializer.java b/app/src/main/java/fi/vanced/utils/ObjectSerializer.java new file mode 100644 index 0000000..1bd3290 --- /dev/null +++ b/app/src/main/java/fi/vanced/utils/ObjectSerializer.java @@ -0,0 +1,83 @@ +package fi.vanced.utils; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications copyright (C) 2022 Vanced + */ + +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +public class ObjectSerializer { + private static final String TAG = "VI - ObjectSerializer"; + + public static String serialize(Serializable obj) throws IOException { + if (obj == null) return ""; + try { + ByteArrayOutputStream serialObj = new ByteArrayOutputStream(); + ObjectOutputStream objStream = new ObjectOutputStream(serialObj); + objStream.writeObject(obj); + objStream.close(); + return encodeBytes(serialObj.toByteArray()); + } catch (Exception e) { + Log.e(TAG, "Serialization error: " + e.getMessage(), e); + throw new IOException(e); + } + } + + public static Object deserialize(String str) throws IOException { + if (str == null || str.length() == 0) return null; + try { + ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str)); + ObjectInputStream objStream = new ObjectInputStream(serialObj); + return objStream.readObject(); + } catch (Exception e) { + Log.e(TAG, "Deserialization error: " + e.getMessage(), e); + throw new IOException(e); + } + } + + public static String encodeBytes(byte[] bytes) { + StringBuffer strBuf = new StringBuffer(); + + for (int i = 0; i < bytes.length; i++) { + strBuf.append((char) (((bytes[i] >> 4) & 0xF) + ((int) 'a'))); + strBuf.append((char) (((bytes[i]) & 0xF) + ((int) 'a'))); + } + + return strBuf.toString(); + } + + public static byte[] decodeBytes(String str) { + byte[] bytes = new byte[str.length() / 2]; + for (int i = 0; i < str.length(); i+=2) { + char c = str.charAt(i); + bytes[i/2] = (byte) ((c - 'a') << 4); + c = str.charAt(i+1); + bytes[i/2] += (c - 'a'); + } + return bytes; + } + +} \ No newline at end of file diff --git a/app/src/main/java/fi/vanced/utils/SharedPrefUtils.java b/app/src/main/java/fi/vanced/utils/SharedPrefUtils.java new file mode 100644 index 0000000..cc3277b --- /dev/null +++ b/app/src/main/java/fi/vanced/utils/SharedPrefUtils.java @@ -0,0 +1,43 @@ +package fi.vanced.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class SharedPrefUtils { + public static void saveString(Context context, String preferenceName, String key, String value){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + sharedPreferences.edit().putString(key, value).apply(); + } + public static void saveBoolean(Context context, String preferenceName, String key, Boolean value){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + sharedPreferences.edit().putBoolean(key, value).apply(); + } + public static void saveInt(Context context, String preferenceName, String key, Integer value){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + sharedPreferences.edit().putInt(key, value).apply(); + } + + public static String getString(Context context, String preferenceName, String key){ + return getString(context, preferenceName, key, null); + } + public static String getString(Context context, String preferenceName, String key, String _default){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + return (sharedPreferences.getString(key, _default)); + } + + public static Boolean getBoolean(Context context, String preferenceName, String key){ + return getBoolean(context, preferenceName, key, false); + } + public static Boolean getBoolean(Context context, String preferenceName, String key, Boolean _default){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + return (sharedPreferences.getBoolean(key, _default)); + } + + public static Integer getInt(Context context, String preferenceName, String key){ + return getInt(context, preferenceName, key, -1); + } + public static Integer getInt(Context context, String preferenceName, String key, Integer _default){ + SharedPreferences sharedPreferences = context.getSharedPreferences(preferenceName, Context.MODE_PRIVATE); + return (sharedPreferences.getInt(key, _default)); + } +} diff --git a/app/src/main/java/fi/vanced/utils/VancedUtils.java b/app/src/main/java/fi/vanced/utils/VancedUtils.java new file mode 100644 index 0000000..e9b0c78 --- /dev/null +++ b/app/src/main/java/fi/vanced/utils/VancedUtils.java @@ -0,0 +1,49 @@ +package fi.vanced.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.google.android.apps.youtube.app.YouTubeTikTokRoot_Application; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.security.SecureRandom; + +public class VancedUtils { + + public static SharedPreferences getPreferences(Context context, String preferencesName) { + if (context == null) return null; + return context.getSharedPreferences(preferencesName, Context.MODE_PRIVATE); + } + + public static String parseJson(HttpURLConnection connection) throws IOException { + StringBuilder jsonBuilder = new StringBuilder(); + InputStream inputStream = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line); + } + inputStream.close(); + return jsonBuilder.toString(); + } + + public static int getIdentifier(String name, String defType) { + Context context = YouTubeTikTokRoot_Application.getAppContext(); + return context.getResources().getIdentifier(name, defType, context.getPackageName()); + } + + // https://stackoverflow.com/a/157202 + static final String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + static SecureRandom rnd = new SecureRandom(); + + public static String randomString(int len){ + StringBuilder sb = new StringBuilder(len); + for(int i = 0; i < len; i++) + sb.append(AB.charAt(rnd.nextInt(AB.length()))); + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/pl/jakubweg/PlayerController.java b/app/src/main/java/pl/jakubweg/PlayerController.java index e884f57..6039bc8 100644 --- a/app/src/main/java/pl/jakubweg/PlayerController.java +++ b/app/src/main/java/pl/jakubweg/PlayerController.java @@ -64,8 +64,6 @@ public class PlayerController { return; } - VideoInformation.currentVideoId = videoId; - Context context = YouTubeTikTokRoot_Application.getAppContext(); if(context == null){ Log.e(TAG, "context is null"); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 129c4ab..d8835b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -333,4 +333,8 @@ Invalid hex code Change Reset + + Copy + TCopy + Ads