Compare commits

..
Author SHA1 Message Date
gdlbo 44fa23020e fixed double pin 2023-09-25 18:19:05 +03:00
gdlbo 4cdcf9aee0 Update R.java 2023-09-25 16:37:31 +03:00
gdlbo 63c5262444 conflicts fixes 2023-09-25 15:45:33 +03:00
gdlbo 9074e80478 Merge branch 'main' into feat/app_pin 2023-09-25 15:26:21 +03:00
gdlbo f7f43928cf cleanup and refactor code
also fixed ts merging
2023-09-25 14:44:27 +03:00
Vologhat 1accb45f12 Merge remote-tracking branch 'origin/main' 2023-09-24 18:43:40 +03:00
Vologhat d9f53920e5 optimize music cache logic 2023-09-24 18:39:12 +03:00
gdlbo 76d91332d7 added recommendation icons 2023-09-23 16:04:45 +03:00
gdlbo 728cbde683 added xiaomi battery dozing information 2023-09-23 14:58:13 +03:00
gdlbo 60c98cd998 some settings improves 2023-09-23 14:50:18 +03:00
gdlbo f1d106e0f0 added gms info in device info
installed microg/gms and push service
2023-09-23 11:17:43 +03:00
gdlbo c92194e90b app.revanced.android.gms support (#1474) 2023-09-23 10:53:05 +03:00
gdlbo 91683af807 deleted useless file 2023-09-23 01:50:43 +03:00
gdlbo c5c898aa4e added recommendations for app fixes + added more gmsless instructions 2023-09-23 01:22:13 +03:00
gdlbo 25634b2469 cache info/clean improvements 2023-09-22 23:27:08 +03:00
gdlbo b8e764d09d Update AboutAppFragment.smali 2023-09-15 19:31:09 +03:00
gdlbo 17ed32857a fixed information about cache settings 2023-09-15 19:05:34 +03:00
gdlbo d631d07e0f removed useless update toast 2023-09-15 19:03:14 +03:00
gdlbo 2ec4c69832 added "Data and storage" item to settings 2023-09-15 18:40:38 +03:00
abslike 6426c965c6 Fix button black text in bots 2023-09-15 18:38:27 +03:00
gdlbo 4f6d410185 stories disabled by default 2023-09-15 18:09:29 +03:00
gdlbo 18a899f439 updated and refactored vt settings 2023-09-15 18:08:28 +03:00
gdlbo 7e5b3fe233 superapp audio item fix 2023-09-14 12:08:16 +03:00
gdlbo 4abd0eec6c fix compile 2023-09-14 11:28:08 +03:00
gdlbo cf17ed64c7 fixed invert in cached playlist 2023-07-30 16:49:30 +03:00
gdlbo a2dd7c1e06 fixed strange code 2023-07-29 17:06:12 +03:00
gdlbo 021fe66bec invert cached tracks list 2023-07-29 16:48:30 +03:00
gdlbo 1b3896f4ca fix colors issue 2023-07-28 19:01:57 +03:00
gdlbo fc9ccf66d1 fix #1409 2023-07-28 18:49:11 +03:00
Vologhat d205f2894b rewrite StickersFragment.java
- move add tgs action to toolbar
- some optimizations
2023-07-14 12:53:57 +03:00
Vologhat 7afc04b2b2 fix tgs packs download 2023-07-14 12:48:45 +03:00
Vologhat 584ee96ee0 fix #1406 2023-07-14 12:37:50 +03:00
gdlbo 66f7ea1b79 Update VTVerifications.java 2023-07-11 21:47:53 +03:00
gdlbo 7b0a83458b Update README.md 2023-07-08 18:03:41 +03:00
gdlbo 13fab86b83 Merge branch 'main' into feat/app_pin 2023-03-25 22:08:20 +03:00
k1zn 5ada5d74d2 /ru/ add time localization 2023-03-12 19:25:06 +05:00
k1zn aa1802c371 /ru/ localization 2023-03-12 19:17:19 +05:00
aliveoutside 0cf1c6ecba Merge remote-tracking branch 'origin/feat/app_pin' into feat/app_pin 2023-03-12 13:38:48 +03:00
gdlbo f051420ff9 upgraded pin-code settings 2023-03-07 19:56:53 +03:00
gdlbo 3902a40e30 Update R.java 2023-03-07 19:16:38 +03:00
aliveoutside 0c97f15088 pincode code cleanup 2023-03-05 17:46:39 +03:00
aliveoutside 4cf112921b open keyboard automatically if fingerprint prompt was dismissed 2023-03-05 16:49:58 +03:00
aliveoutside 6e31b26af0 remove pincode time setting and set the default timer to 24 hrs 2023-03-05 16:15:32 +03:00
gdlbo 6f22b5dd03 pin number limits 2023-03-03 20:22:05 +03:00
gdlbo 9ee1bd0f79 Update UnlockActivity.java 2023-03-03 16:39:06 +03:00
gdlbo 56b11bc43c fixed size of pin 2023-03-03 16:22:44 +03:00
gdlbo a476d490a6 added something
idk lol lmao dead
2023-03-02 19:15:12 +03:00
gdlbo 74b50fb9c2 reset pin 2023-03-02 13:08:47 +03:00
gdlbo ddaedd0ba0 Improved screen locking system + improved lock display 2023-03-02 12:53:46 +03:00
gdlbo 729342085a compiling fix 2023-03-02 12:07:56 +03:00
gdlbo bb20178dc9 Merge branch 'main' into feat/app_pin 2023-03-02 11:59:41 +03:00
aliveoutside 306cee1fd4 Merge branch 'main' into feat/app_pin 2023-02-20 21:10:07 +03:00
aliveoutside 02e8732e9f init PIN Lock 2023-02-20 21:02:28 +03:00
140 changed files with 4742 additions and 1440 deletions
+1 -1
View File
@@ -37,7 +37,7 @@
* Стабильные сборки находятся в [Releases](https://github.com/vtosters/lite/releases)
Зеркало на нашем [<img src="https://gitea.com/assets/img/logo.svg" align="center" width="20" height="20"/> Git](https://git.maki.su/gdlbo/lite)
Зеркало на нашем [<img src="https://gitea.com/assets/img/logo.svg" align="center" width="20" height="20"/> Git](https://git.maki.su/vtosters/lite)
### Быстрая навигация
+6
View File
@@ -84,6 +84,12 @@ android {
}
}
task testClasses {
doLast {
println 'This is a dummy testClasses task'
}
}
dependencies {
implementation 'com.microsoft.appcenter:appcenter-analytics:5.0.1'
implementation 'com.microsoft.appcenter:appcenter-crashes:5.0.1'
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -43,4 +43,5 @@
-keep class com.vtosters.lite.** { *; }
-keep class com.vk.** { *; }
-keep class bruhcollective.itaysonlab.libvkx.client.** { *; }
-keep class bruhcollective.itaysonlab.libvkx.client.** { *; }
-keep class com.guardanis.applock.** { *; }
@@ -10,9 +10,9 @@ import com.vk.music.common.MusicPlaybackLaunchContext;
import java.util.ArrayList;
import java.util.List;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.hooks.other.Preferences.getBoolValue;
import static ru.vtosters.hooks.other.Preferences.isValidSignature;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
public class LibVKXClient {
@SuppressLint("StaticFieldLeak")
@@ -80,11 +80,6 @@ public class LibVKXClient {
lambdaplay(list, musicTrack, iLibVkxService, null);
}
public static String asId(MusicTrack musicTrack) {
String id = musicTrack.e + "_" + musicTrack.d;
if (musicTrack.O != null && !musicTrack.O.equals("")) {
id += "_" + musicTrack.O;
}
return id;
}
public static String asId(MusicTrack musicTrack)
{ return musicTrack.y1(); }
}
@@ -36,10 +36,8 @@ public class TelegramStickersPack extends CustomStickersPack {
public HashMap<String, List<Integer>> emojis = new HashMap<>();
public TelegramStickersPack(String id) {
final var idLowerCase = id.toLowerCase();
this.id = idLowerCase;
this.hash = idLowerCase.hashCode();
title = id;
this.id=this.title=id;
this.hash=id.hashCode();
}
public JSONObject encodeEmojis() throws JSONException {
@@ -103,7 +101,7 @@ public class TelegramStickersPack extends CustomStickersPack {
@Override
public boolean equals(Object obj) {
return obj instanceof TelegramStickersPack && ((TelegramStickersPack) obj).id.equalsIgnoreCase(id);
return obj instanceof TelegramStickersPack && ((TelegramStickersPack) obj).id.equals(id);
}
@Override
@@ -8,6 +8,7 @@ import android.util.Log;
import com.aefyr.tsg.g2.sql.TelegramStickersDbHelper;
import com.aefyr.tsg.g2.stickersgrabber.TelegramStickersGrabber;
import com.aefyr.tsg.g2.stickersgrabber.TelegramStickersPackInfo;
import ru.vtosters.lite.ui.fragments.VTSettings;
import ru.vtosters.lite.ui.fragments.tgstickers.StickersFragment;
import java.io.File;
@@ -152,6 +153,7 @@ public class TelegramStickersService {
private void notifyActivePacksListChanged() {
getGlobalContext().sendBroadcast(new Intent(StickersFragment.ACTION_RELOAD));
getGlobalContext().sendBroadcast(new Intent(VTSettings.ACTION_INVALIDATE_TGS_COUNT));
if (listeners.isEmpty())
return;
@@ -222,7 +224,7 @@ public class TelegramStickersService {
final TelegramStickersPack newPack = pack;
final boolean isUpdate = update;
currentlyDownloading.add(id.toLowerCase());
currentlyDownloading.add(id);
notificationsHelper.packStartedDownloading(newPack);
@@ -0,0 +1,342 @@
package com.guardanis.applock;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import com.vtosters.lite.R;
import com.guardanis.applock.activities.UnlockActivity;
import com.guardanis.applock.services.FingerprintLockService;
import com.guardanis.applock.services.FingerprintLockService.AuthenticationDelegate;
import com.guardanis.applock.services.LockService;
import com.guardanis.applock.services.PINLockService;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import ru.vtosters.lite.utils.AndroidUtils;
public class AppLock {
public static final int REQUEST_CODE_UNLOCK = 9371;
public static final int REQUEST_CODE_LOCK_CREATION = 9372;
public static final int REQUEST_CODE_FINGERPRINT_PERMISSION = 9373;
public static final int ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE = 1;
public static final int ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED = 2;
public static final int ERROR_CODE_FINGERPRINTS_EMPTY = 3;
public static final int ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED = 4;
public static final int ERROR_CODE_SDK_VERSION_MINIMUM = 5;
private static final String PREFS = "pin__preferences";
private static final String PREF_UNLOCK_FAILURE_TIME = "pin__unlock_failure_time";
private static final String PREF_UNLOCK_SUCCESS_TIME = "pin__unlock_success_time";
private static AppLock instance;
protected Context context;
protected HashMap<Class<? extends LockService>, LockService> lockServices = new HashMap<>();
protected int unlockAttemptsCount = 1;
protected AppLock(Context context) {
this.context = context.getApplicationContext();
this.lockServices.put(PINLockService.class, new PINLockService());
this.lockServices.put(FingerprintLockService.class, new FingerprintLockService());
}
public static AppLock getInstance(Context context) {
if (instance == null)
instance = new AppLock(context);
return instance;
}
/**
* @return true if the user has enrolled in either PIN or Fingerprint locking
*/
public static boolean isEnrolled(Context context) {
AppLock helper = getInstance(context);
for (LockService service : helper.lockServices.values()) {
if (service.isEnrolled(context))
return true;
}
return false;
}
public static void setAuthenticationRequired(Context context) {
AppLock helper = getInstance(context);
helper.getPreferences()
.edit()
.putLong(PREF_UNLOCK_SUCCESS_TIME, 0)
.commit();
}
/**
* @return true if the user is enrolled in locking and the last successful unlock happened more than the default lock duration ago
*/
public static boolean isUnlockRequired(Context context) {
int minutes = Integer.parseInt(getPinCodeTime(context));
if (minutes < 1) {
minutes = 1;
}
return isUnlockRequired(context, TimeUnit.MINUTES.toMillis(minutes));
}
public static String getPinCodeTime(Context context) {
return context.getString(AndroidUtils.getIdentifier("applock__activity_lock_reenable_minutes", "integer"));
}
public static String getPinCodeLimits(Context context) {
return context.getString(AndroidUtils.getIdentifier("applock__input_pin_item_count", "integer"));
}
/**
* @return true if the user is enrolled in locking and the last successful unlock happened more than lastSuccessValidMs ago
*/
public static boolean isUnlockRequired(Context context, long lastSuccessValidMs) {
return isEnrolled(context) && lastSuccessValidMs < System.currentTimeMillis() - getUnlockSuccessTime(context);
}
protected static long getUnlockSuccessTime(Context context) {
return AppLock.getInstance(context)
.getPreferences()
.getLong(PREF_UNLOCK_SUCCESS_TIME, 0);
}
public static void onActivityResumed(Activity activity) {
if (isUnlockRequired(activity)) {
boolean unlockActivityReturnAllowed = activity.getResources()
.getBoolean(AndroidUtils.getIdentifier("applock__unlock_activity_return_allowed", "bool"));
Intent intent = new Intent(activity, UnlockActivity.class)
.putExtra(UnlockActivity.INTENT_ALLOW_UNLOCKED_EXIT, unlockActivityReturnAllowed);
activity.startActivityForResult(intent, REQUEST_CODE_UNLOCK);
}
}
/**
* Check if an action-based unlock is required and navigates to the UnlockActivity if true.
*
* @return true if unlock is required.
*/
public static boolean unlockIfRequired(Activity activity) {
if (isUnlockRequired(activity)) {
Intent intent = new Intent(activity, UnlockActivity.class)
.putExtra(UnlockActivity.INTENT_ALLOW_UNLOCKED_EXIT, true);
activity.startActivityForResult(intent, REQUEST_CODE_UNLOCK);
return true;
} else
return false;
}
public void attemptFingerprintUnlock(final UnlockDelegate eventListener) {
if (handleFailureBlocking(eventListener))
return;
AuthenticationDelegate delegate = new AuthenticationDelegate() {
@Override
public void onResolutionRequired(int errorCode) {
// eventListener.onResolutionRequired(errorCode);
eventListener.requestOpenKeyboard();
}
@Override
public void onAuthenticationHelp(int code, CharSequence message) {
eventListener.onAuthenticationHelp(code, String.valueOf(message));
}
@Override
public void onAuthenticating(CancellationSignal cancellationSignal) {
// Handled internally
}
@Override
public void onAuthenticationSuccess(@NonNull FingerprintManagerCompat.AuthenticationResult result) {
onUnlockSuccessful(eventListener);
}
@Override
public void onAuthenticationFailed(String message) {
Log.w(getClass().getSimpleName(), "Fingerprint authentication failed: " + message);
}
};
getLockService(FingerprintLockService.class)
.authenticate(context, delegate);
}
public void attemptPINUnlock(String pin, final UnlockDelegate eventListener) {
if (handleFailureBlocking(eventListener))
return;
PINLockService.AuthenticationDelegate delegate = new PINLockService.AuthenticationDelegate() {
@Override
public void onNoPIN() {
onUnlockFailed(context.getString(AndroidUtils.getIdentifier("applock__unlock_error_no_matching_pin_found", "string")));
}
@Override
public void onPINDoesNotMatch() {
onUnlockFailed(context.getString(AndroidUtils.getIdentifier("applock__unlock_error_match_failed", "string")));
}
@Override
public void onPINMatches() {
onUnlockSuccessful(eventListener);
}
private void onUnlockFailed(String message) {
handleUnlockFailure(message, eventListener);
}
};
getLockService(PINLockService.class)
.authenticate(context, pin, delegate);
}
/**
* @return true if failure blocking is enabled
*/
private boolean handleFailureBlocking(final UnlockDelegate eventListener) {
if (isUnlockFailureBlockEnabled()) {
unlockAttemptsCount++;
if (getFailureDelayMs() < System.currentTimeMillis() - getUnlockFailureBlockStart())
resetUnlockFailure();
else {
String message = String.format(
context.getString(AndroidUtils.getIdentifier("applock__unlock_error_retry_limit_exceeded", "string")),
formatTimeRemaining());
if (eventListener != null)
eventListener.onFailureLimitExceeded(message);
return true;
}
}
return false;
}
protected void handleUnlockFailure(String message, UnlockDelegate eventListener) {
this.unlockAttemptsCount++;
if (eventListener != null)
eventListener.onFailureLimitExceeded(message);
if (context.getResources().getInteger(AndroidUtils.getIdentifier("applock__max_retry_count", "integer")) < unlockAttemptsCount)
onFailureExceedsLimit();
}
public SharedPreferences getPreferences() {
return context.getSharedPreferences(PREFS, 0);
}
protected void onFailureExceedsLimit() {
getPreferences()
.edit()
.putLong(PREF_UNLOCK_FAILURE_TIME, System.currentTimeMillis())
.commit();
}
public boolean isUnlockFailureBlockEnabled() {
return context.getResources().getInteger(AndroidUtils.getIdentifier("applock__max_retry_count", "integer")) < unlockAttemptsCount
|| System.currentTimeMillis() - getUnlockFailureBlockStart() < getFailureDelayMs();
}
protected long getUnlockFailureBlockStart() {
return getPreferences()
.getLong(PREF_UNLOCK_FAILURE_TIME, 0);
}
protected void onUnlockSuccessful(UnlockDelegate eventListener) {
getPreferences()
.edit()
.putLong(PREF_UNLOCK_SUCCESS_TIME, System.currentTimeMillis())
.commit();
resetUnlockFailure();
if (eventListener != null)
eventListener.onUnlockSuccessful();
}
/**
* Reset the last login time to require the user to re-authenticate with
* AppLock the next time they are eligible to unlock.
*/
public void setAuthenticationRequired() {
getPreferences()
.edit()
.putLong(PREF_UNLOCK_SUCCESS_TIME, 0)
.commit();
}
protected void resetUnlockFailure() {
unlockAttemptsCount = 1;
getPreferences()
.edit()
.putLong(PREF_UNLOCK_FAILURE_TIME, 0)
.commit();
}
protected String formatTimeRemaining() {
long millis = getFailureDelayMs() - (System.currentTimeMillis() - getUnlockFailureBlockStart());
long seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis));
if (TimeUnit.MILLISECONDS.toMinutes(millis) < 1)
return String.format(AndroidUtils.getString(R.string.n_seconds), seconds);
else
return String.format(AndroidUtils.getString(R.string.n_mins_secs), TimeUnit.MILLISECONDS.toMinutes(millis), seconds);
}
protected long getFailureDelayMs() {
return TimeUnit.MINUTES.toMillis(context.getResources()
.getInteger(AndroidUtils.getIdentifier("applock__failure_retry_delay", "integer")));
}
/**
* This will remove all PIN and/or Fingerprint enrollment data.
* Users will need to re-enroll in AppLock after this call.
*/
public void invalidateEnrollments() {
resetUnlockFailure();
setAuthenticationRequired();
for (LockService service : lockServices.values())
service.invalidateEnrollments(context);
}
public void cancelPendingAuthentications() {
for (LockService service : lockServices.values())
service.cancelPendingAuthentications(context);
}
public <T extends LockService> T getLockService(Class<T> named) {
return (T) lockServices.get(named);
}
public interface UnlockDelegate {
void onUnlockSuccessful();
void onResolutionRequired(int errorCode);
void onAuthenticationHelp(int code, String message);
void onFailureLimitExceeded(String message);
void requestOpenKeyboard();
}
}
@@ -0,0 +1,58 @@
package com.guardanis.applock.activities;
import static ru.vtosters.hooks.other.ThemesUtils.getBackgroundContent;
import static ru.vtosters.hooks.other.ThemesUtils.getTextAttr;
import static ru.vtosters.hooks.other.ThemesUtils.isDarkTheme;
import static ru.vtosters.lite.utils.AndroidUtils.getIdentifier;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsetsController;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.guardanis.applock.views.LockCreationViewController;
public class LockCreationActivity extends AppCompatActivity implements LockCreationViewController.Delegate {
protected LockCreationViewController viewController;
@Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
setContentView(getIdentifier("applock__activity_lock_creation", "layout"));
findViewById(getIdentifier("applock__activity_lock_creation", "id")).setBackgroundColor(getBackgroundContent());
((TextView) findViewById(getIdentifier("pin__description", "id"))).setTextColor(getTextAttr());
this.viewController = new LockCreationViewController(this, findViewById(getIdentifier("pin__container", "id")));
this.viewController.setDelegate(this);
this.viewController.setupRootFlow();
this.getWindow().setStatusBarColor(getBackgroundContent());
this.getWindow().setNavigationBarColor(getBackgroundContent());
if (!isDarkTheme()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.getWindow().getInsetsController().setSystemBarsAppearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
} else {
this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
}
}
}
@Override
public void onLockCreated() {
setResult(Activity.RESULT_OK);
finish();
}
@Override
public void onPointerCaptureChanged(boolean hasCapture) {
super.onPointerCaptureChanged(hasCapture);
}
}
@@ -0,0 +1,31 @@
package com.guardanis.applock.activities;
import android.app.Activity;
import android.content.Intent;
import androidx.appcompat.app.AppCompatActivity;
import com.guardanis.applock.AppLock;
public class LockableAppCompatActivity extends AppCompatActivity {
@Override
protected void onPostResume() {
super.onPostResume();
AppLock.onActivityResumed(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == AppLock.REQUEST_CODE_UNLOCK && resultCode == Activity.RESULT_CANCELED)
handleUnlockActivityReturnFailure();
}
protected void handleUnlockActivityReturnFailure() {
setResult(Activity.RESULT_CANCELED);
finish();
}
}
@@ -0,0 +1,88 @@
package com.guardanis.applock.activities;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowInsetsController;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.guardanis.applock.views.UnlockViewController;
import ru.vtosters.lite.utils.AndroidUtils;
import static ru.vtosters.hooks.other.ThemesUtils.getBackgroundContent;
import static ru.vtosters.hooks.other.ThemesUtils.getTextAttr;
import static ru.vtosters.hooks.other.ThemesUtils.isDarkTheme;
public class UnlockActivity extends AppCompatActivity implements UnlockViewController.Delegate {
public static final String INTENT_ALLOW_UNLOCKED_EXIT = "pin__allow_activity_exit"; // false by default
protected UnlockViewController viewController;
@Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
setContentView(AndroidUtils.getIdentifier("applock__activity_unlock", "layout"));
findViewById(AndroidUtils.getIdentifier("applock__activity_unlock", "id")).setBackgroundColor(getBackgroundContent());
((TextView) findViewById(AndroidUtils.getIdentifier("pin__description", "id"))).setTextColor(getTextAttr());
this.viewController = new UnlockViewController(this, findViewById(AndroidUtils.getIdentifier("pin__container", "id")));
this.viewController.setAutoAuthorizationEnabled(false); // Disable auto authorization so fingerprint doesn't crash onResume
this.viewController.setDelegate(this);
this.viewController.setupRootFlow();
this.viewController.setAutoAuthorizationEnabled(true);
this.getWindow().setStatusBarColor(getBackgroundContent());
this.getWindow().setNavigationBarColor(getBackgroundContent());
if (!isDarkTheme()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
this.getWindow().getInsetsController().setSystemBarsAppearance(WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS);
} else {
this.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR & View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
}
}
}
@Override
public void onUnlockSuccessful() {
setResult(Activity.RESULT_OK);
finish();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (event.getRepeatCount() == 0) handleBackPressed();
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public void onBackPressed() {
handleBackPressed();
}
protected void handleBackPressed() {
if (!getIntent().getBooleanExtra(INTENT_ALLOW_UNLOCKED_EXIT, false)) {
return;
}
setResult(Activity.RESULT_CANCELED);
finish();
}
@Override
public void onPointerCaptureChanged(boolean hasCapture) {
super.onPointerCaptureChanged(hasCapture);
}
}
@@ -0,0 +1,104 @@
package com.guardanis.applock.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.util.Log;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialog;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.views.AppLockViewController;
import java.lang.ref.WeakReference;
public abstract class AppLockDialogBuilder<ALVC extends AppLockViewController> {
protected WeakReference<Activity> activity;
protected ALVC viewController;
protected WeakReference<AppCompatDialog> dialog = new WeakReference<AppCompatDialog>(null);
protected int layoutResId;
public AppLockDialogBuilder(Activity activity, int layoutResId) {
this.activity = new WeakReference<>(activity);
this.layoutResId = layoutResId;
}
public Dialog show() {
if (viewController != null)
throw new RuntimeException("You can only call show() once per AppLockDialogBuilder instance.");
final Activity activity = this.activity.get();
if (activity == null)
return null;
View parent = activity.getLayoutInflater()
.inflate(layoutResId, null, false);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setView(parent);
builder.setOnCancelListener(dialogInterface -> handleCanceled());
builder.setOnDismissListener(dialogInterface -> handleDismissed());
this.viewController = buildViewControllerInstance(parent);
this.viewController.setupRootFlow();
AppCompatDialog dialog = builder.show();
this.dialog = new WeakReference<>(dialog);
return dialog;
}
protected abstract ALVC buildViewControllerInstance(View parent);
protected void handleCanceled() {
dismissDialog();
}
protected void handleDismissed() {
this.dialog = new WeakReference<>(null);
this.viewController.unregisterReceivers();
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.cancelPendingAuthentications();
}
protected void dismissDialog() {
try {
AppCompatDialog dialog = this.dialog.get();
if (dialog == null)
return;
// Attempt to prevent stupid fucking uncatchable system error "View not Attached to Window" when dialog dismissed
Context context = ((ContextWrapper) dialog.getContext()).getBaseContext();
if (context instanceof Activity) {
if (!((Activity) context).isFinishing()) {
if (!((Activity) context).isDestroyed())
dialog.dismiss();
}
} else
dialog.dismiss();
} catch (Throwable e) {
Log.e("AppLockDialogBuilder", "View not Attached to Window???", e);
}
this.dialog = new WeakReference<>(null);
}
public ALVC getViewController() {
return viewController;
}
}
@@ -0,0 +1,60 @@
package com.guardanis.applock.dialogs;
import android.app.Activity;
import android.view.View;
import com.guardanis.applock.views.LockCreationViewController;
import ru.vtosters.lite.utils.AndroidUtils;
public class LockCreationDialogBuilder extends AppLockDialogBuilder<LockCreationViewController> implements LockCreationViewController.Delegate {
protected Runnable lockCreatedCallback;
protected Runnable canceledCallback;
public LockCreationDialogBuilder(Activity activity) {
super(activity, AndroidUtils.getIdentifier("applock__lock_creation", "layout"));
}
/**
* Set a Runnable to be triggered when a user has successfully enrolled in locking.
*/
public LockCreationDialogBuilder onLockCreated(Runnable lockCreatedCallback) {
this.lockCreatedCallback = lockCreatedCallback;
return this;
}
/**
* Set a Runnable to be triggered when a user has canceled enrollment.
*/
public LockCreationDialogBuilder onCanceled(Runnable canceledCallback) {
this.canceledCallback = canceledCallback;
return this;
}
@Override
protected LockCreationViewController buildViewControllerInstance(View parent) {
LockCreationViewController controller = new LockCreationViewController(activity.get(), parent);
controller.setDelegate(this);
return controller;
}
@Override
public void onLockCreated() {
dismissDialog();
if (lockCreatedCallback != null)
lockCreatedCallback.run();
}
@Override
protected void handleCanceled() {
super.handleCanceled();
if (canceledCallback != null)
canceledCallback.run();
}
}
@@ -0,0 +1,110 @@
package com.guardanis.applock.dialogs;
import android.app.Activity;
import android.app.Dialog;
import android.view.View;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.views.UnlockViewController;
import ru.vtosters.lite.utils.AndroidUtils;
public class UnlockDialogBuilder extends AppLockDialogBuilder<UnlockViewController> implements UnlockViewController.Delegate {
protected Runnable unlockCallback;
protected Runnable canceledCallback;
public UnlockDialogBuilder(Activity activity) {
super(activity, AndroidUtils.getIdentifier("applock__unlock", "layout"));
}
/**
* Set a Runnable to be triggered when a user has successfully unlocked.
*/
public UnlockDialogBuilder onUnlocked(Runnable unlockCallback) {
this.unlockCallback = unlockCallback;
return this;
}
/**
* Set a Runnable to be triggered when a user has canceled unlocking.
*/
public UnlockDialogBuilder onCanceled(Runnable canceledCallback) {
this.canceledCallback = canceledCallback;
return this;
}
@Override
protected UnlockViewController buildViewControllerInstance(View parent) {
UnlockViewController controller = new UnlockViewController(activity.get(), parent);
controller.setDelegate(this);
return controller;
}
@Override
public void onUnlockSuccessful() {
dismissDialog();
triggerUnlockCallback();
}
@Override
protected void handleCanceled() {
super.handleCanceled();
if (canceledCallback != null)
canceledCallback.run();
}
protected void triggerUnlockCallback() {
if (unlockCallback != null)
unlockCallback.run();
}
/**
* Check if the user is actually enrolled in locking before opening the unlock dialog.
* If the user is enrolled, show() will be called. Otherwise, this function will trigger this
* instance's UnlockDialogBuilder.unlockCallback immediately.
*
* @return Dialog if show() is called
*/
public Dialog showIfEnrolledOrSuccess() {
Activity activity = this.activity.get();
if (activity == null)
return null;
if (!AppLock.isEnrolled(activity)) {
triggerUnlockCallback();
return null;
}
return show();
}
/**
* Check if the user is both enrolled in locking and that unlocking is required before
* opening the unlock dialog. If the user is enrolled, show() will be called. Otherwise,
* this function will trigger this instance's UnlockDialogBuilder.unlockCallback immediately.
*
* @param longValidDurationMs the milliseconds a user's last unlock is valid for
* @return Dialog if show() is called
*/
public Dialog showIfRequiredOrSuccess(long longValidDurationMs) {
Activity activity = this.activity.get();
if (activity == null)
return null;
if (!AppLock.isUnlockRequired(activity, longValidDurationMs)) {
triggerUnlockCallback();
return null;
}
return show();
}
}
@@ -0,0 +1,116 @@
package com.guardanis.applock.pin;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.TextView;
import com.guardanis.applock.AppLock;
import ru.vtosters.lite.utils.AndroidUtils;
import java.lang.ref.WeakReference;
interface InputTextListener {
void onTextEntered(int count);
}
public class PINInputController implements TextView.OnEditorActionListener, InputTextListener {
private final WeakReference<PINInputView> inputView;
private WeakReference<InputEventListener> eventListener = new WeakReference<>(null);
/**
* If you want to actually receive events after this, make sure you call setInputEventListener
*/
public PINInputController(PINInputView inputView) {
this.inputView = new WeakReference<>(inputView);
inputView.setOnEditorActionListener(this);
inputView.setInputTextListener(this);
}
public PINInputController ensureKeyboardVisible() {
final PINInputView inputView = this.inputView.get();
if (inputView == null)
return this;
inputView.postDelayed(inputView::ensureKeyboardVisible, 300);
return this;
}
public PINInputController setPasswordCharactersEnabled(boolean passwordCharacterEnabled) {
this.inputView.get().setPasswordCharactersEnabled(passwordCharacterEnabled);
return this;
}
public PINInputController setInputNumbersCount(int inputNumbersCount) {
this.inputView.get().setInputViewsCount(inputNumbersCount);
return this;
}
public PINInputController setInputEventListener(InputEventListener eventListener) {
this.eventListener = new WeakReference<>(eventListener);
return this;
}
@Override
public boolean onEditorAction(TextView textView, int i, KeyEvent keyEvent) {
PINInputView inputView = this.inputView.get();
if (inputView == null)
return false;
if (isSoftKeyboardFinishedAction(textView, i, keyEvent)) {
InputEventListener eventListener = this.eventListener.get();
if (eventListener != null)
eventListener.onInputEntered(inputView.getText());
inputView.reset();
return true;
}
return false;
}
private boolean isSoftKeyboardFinishedAction(TextView view, int action, KeyEvent event) {
// Some devices return null event on editor actions for Enter Button (thanks, Samsung)
if (!(event == null || event.getAction() == KeyEvent.ACTION_DOWN))
return false;
// Enter clicked on Bluetooth Keyboard (action is zero on hardware keyboards)
if (action == EditorInfo.IME_ACTION_UNSPECIFIED && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)
return true;
return action == EditorInfo.IME_ACTION_DONE
|| action == EditorInfo.IME_ACTION_GO
|| action == EditorInfo.IME_ACTION_SEND;
}
public boolean matchesRequiredPINLength(String input) {
final PINInputView inputView = this.inputView.get();
if (inputView == null)
return false;
return inputView.matchesRequiredPINLength(input);
}
@Override
public void onTextEntered(int count) {
if (count == Integer.parseInt(AppLock.getPinCodeLimits(AndroidUtils.getGlobalContext()))) {
var inputView = this.inputView.get();
InputEventListener eventListener = this.eventListener.get();
if (eventListener != null)
eventListener.onInputEntered(inputView.getText());
inputView.reset();
}
}
public interface InputEventListener {
void onInputEntered(String input);
}
}
@@ -0,0 +1,232 @@
package com.guardanis.applock.pin;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.WeakHashMap;
import com.guardanis.applock.AppLock;
import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.utils.AndroidUtils;
public class PINInputView extends LinearLayout implements TextWatcher {
private final WeakHashMap<PINItemView, PINItemAnimator> animators = new WeakHashMap<PINItemView, PINItemAnimator>();
private Paint itemTextPaint;
private Paint itemBackgroundPaint;
private int inputViewsCount = 10;
private PINItemView[] pinItemViews;
private EditText editText;
private String lastText = "";
private boolean passwordCharactersEnabled = true;
private String passwordCharacter;
private InputTextListener listener;
public PINInputView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
private void init(AttributeSet attrs) {
setWillNotDraw(false);
setupEditText();
itemTextPaint = new Paint();
itemTextPaint.setColor(ThemesUtils.getAccentColor());
itemBackgroundPaint = new Paint();
itemBackgroundPaint.setColor(ThemesUtils.getMutedAccentColor());
inputViewsCount = Integer.parseInt(AppLock.getPinCodeLimits(AndroidUtils.getGlobalContext()));
passwordCharacter = getResources().getString(AndroidUtils.getIdentifier("applock__password_char", "string"));
}
protected void setupEditText() {
removeAllViews();
this.editText = inflateFakeEditText();
this.editText.addTextChangedListener(this);
addView(editText);
}
protected EditText inflateFakeEditText() {
return (EditText) LayoutInflater.from(getContext())
.inflate(AndroidUtils.getIdentifier("applock__fake_edit_text", "layout"), this, false);
}
public PINInputView setInputViewsCount(int inputViewsCount) {
this.inputViewsCount = inputViewsCount;
return reset();
}
public PINInputView reset() {
this.lastText = "";
this.editText.setText("");
if (pinItemViews != null)
animateLastOut();
return this;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
ensureKeyboardVisible();
return true;
}
return super.onTouchEvent(event);
}
public void ensureKeyboardVisible() {
editText.requestFocus();
((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE))
.showSoftInput(editText, 0);
}
@Override
public void onDraw(Canvas canvas) {
if (pinItemViews == null || pinItemViews.length != inputViewsCount)
setupItemViews(canvas);
String text = editText.getText().toString();
for (int i = 0; i < pinItemViews.length; i++) {
String textItem = i < text.length()
? (passwordCharactersEnabled ? passwordCharacter : text.substring(i, i + 1))
: "";
pinItemViews[i].draw(canvas, textItem);
}
}
private void setupItemViews(Canvas canvas) {
pinItemViews = new PINItemView[inputViewsCount];
int cellWidth = canvas.getWidth() / inputViewsCount;
int largestRadius = Math.min(cellWidth / 2, canvas.getHeight() / 2);
int smallestRadius = (int) (largestRadius * Float.parseFloat(getResources().getString(AndroidUtils.getIdentifier("applock__empty_item_min_size_percent", "string"))));
int[] minMaxRadius = new int[]{smallestRadius, largestRadius};
itemTextPaint.setTextSize((int) (largestRadius * .85));
for (int i = 0; i < pinItemViews.length; i++) {
float[] positionInCanvas = getPositionInCanvas(canvas, i, cellWidth);
pinItemViews[i] = new PINItemView(positionInCanvas, minMaxRadius, itemTextPaint, itemBackgroundPaint);
}
}
private float[] getPositionInCanvas(Canvas canvas, int position, int cellWidth) {
return new float[]{
(cellWidth * position) + (cellWidth / 2),
canvas.getHeight() / 2
};
}
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}
@Override
public void afterTextChanged(Editable editable) {
if (editable == null || lastText == null)
return;
String text = editable.toString();
if (inputViewsCount < text.length()) {
editText.setText(lastText);
editText.setSelection(editText.getText().length());
} else if (text.length() < lastText.length()) {
animateLastOut();
this.lastText = text;
} else if (lastText.length() < text.length()) {
animateLastIn();
this.lastText = text;
}
if (listener != null) {
listener.onTextEntered(text.length());
}
}
public void setInputTextListener(InputTextListener listener) {
this.listener = listener;
}
private void animateLastOut() {
int startingIndex = editText.getText().toString().length(); // One after current length, only happens on backspace
for (int i = pinItemViews.length - 1; startingIndex <= i; i--)
if (!pinItemViews[i].isAnimatedOut())
animate(pinItemViews[i], PINItemAnimator.ItemAnimationDirection.OUT);
}
private void animateLastIn() {
PINItemView item = pinItemViews[editText.getText().toString().length() - 1];
animate(item, PINItemAnimator.ItemAnimationDirection.IN);
}
private void animate(PINItemView view, PINItemAnimator.ItemAnimationDirection direction) {
cancelPreviousAnimation(view);
view.setAnimationDirection(direction);
PINItemAnimator animator = new PINItemAnimator(this, view, direction);
animators.put(view, animator);
animator.start();
}
private void cancelPreviousAnimation(PINItemView view) {
PINItemAnimator animator = animators.get(view);
try {
animator.cancel();
animators.put(view, null);
} catch (Exception e) {
e.printStackTrace();
}
}
public String getText() {
return editText.getText().toString();
}
public void setOnEditorActionListener(TextView.OnEditorActionListener actionListener) {
editText.setOnEditorActionListener(actionListener);
}
public void setPasswordCharactersEnabled(boolean passwordCharactersEnabled) {
this.passwordCharactersEnabled = passwordCharactersEnabled;
}
public void setPasswordCharacter(String passwordCharacter) {
this.passwordCharacter = passwordCharacter;
}
public boolean matchesRequiredPINLength(String input) {
return input.length() == inputViewsCount;
}
}
@@ -0,0 +1,92 @@
package com.guardanis.applock.pin;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import java.lang.ref.WeakReference;
import ru.vtosters.lite.utils.AndroidUtils;
public class PINItemAnimator extends Thread {
private static final int ANIMATION_DURATION = 150;
private static final int UPDATE_RATE = 25;
private final WeakReference<PINInputView> inputView;
private final PINItemView itemView;
private final ItemAnimationDirection animationDirection;
private final Interpolator itemInterpolator = new AccelerateDecelerateInterpolator();
private final float minSizePercent;
private long startTime;
private boolean canceled = false;
public PINItemAnimator(PINInputView inputView, PINItemView itemView, ItemAnimationDirection animationDirection) {
this.inputView = new WeakReference<>(inputView);
this.itemView = itemView;
this.animationDirection = animationDirection;
this.minSizePercent = Float.parseFloat(inputView.getResources().getString(AndroidUtils.getIdentifier("applock__empty_item_min_size_percent", "string")));
}
@Override
public void run() {
this.startTime = System.currentTimeMillis();
try {
if (animationDirection == ItemAnimationDirection.IN)
animateIn();
else
animateOut();
} catch (Exception e) {
e.printStackTrace();
}
}
private void animateIn() throws Exception {
float percent = minSizePercent;
while (percent < 1 && !canceled) {
percent = Math.min(minSizePercent + calculatePercentComplete(), 1);
updateView(percent);
Thread.sleep(UPDATE_RATE);
}
}
private void animateOut() throws Exception {
float percent = 1 - calculatePercentComplete();
while (minSizePercent < percent && !canceled) {
percent = Math.max(1 - calculatePercentComplete(), minSizePercent);
updateView(percent);
Thread.sleep(UPDATE_RATE);
}
}
protected float calculatePercentComplete() {
float completed = ((float) (System.currentTimeMillis() - startTime)) / ANIMATION_DURATION;
return itemInterpolator.getInterpolation(completed);
}
private void updateView(final float percent) {
final PINInputView inputView = this.inputView.get();
if (inputView == null)
return;
inputView.post(() -> {
itemView.onAnimationUpdate(percent);
inputView.invalidate();
});
}
public void cancel() {
this.canceled = true;
}
public enum ItemAnimationDirection {
IN, OUT
}
}
@@ -0,0 +1,61 @@
package com.guardanis.applock.pin;
import android.graphics.Canvas;
import android.graphics.Paint;
public class PINItemView {
private final float[] position;
private final int intendedRadius;
private int currentRadius;
private Paint backgroundPaint;
private float[] textPosition;
private Paint textPaint;
private PINItemAnimator.ItemAnimationDirection animationDirection = PINItemAnimator.ItemAnimationDirection.IN;
public PINItemView(float[] position, int[] minMaxRadius, Paint baseTextPaint, Paint baseBackgroundPaint) {
this.position = position;
this.intendedRadius = minMaxRadius[1];
this.currentRadius = minMaxRadius[0];
setupPaints(baseTextPaint, baseBackgroundPaint);
}
private void setupPaints(Paint baseTextPaint, Paint baseBackgroundPaint) {
this.textPaint = new Paint();
this.textPaint.setTextAlign(Paint.Align.CENTER);
this.textPaint.setAntiAlias(true);
this.textPaint.setColor(baseTextPaint.getColor());
this.textPaint.setTextSize(baseTextPaint.getTextSize());
this.textPosition = new float[]{
position[0],
position[1] - ((textPaint.descent() + textPaint.ascent()) / 2)
};
this.backgroundPaint = new Paint();
this.backgroundPaint.setTextAlign(Paint.Align.CENTER);
this.backgroundPaint.setAntiAlias(true);
this.backgroundPaint.setColor(baseBackgroundPaint.getColor());
}
public void draw(Canvas canvas, String textValue) {
canvas.drawCircle(position[0], position[1], currentRadius, backgroundPaint);
canvas.drawText(textValue, textPosition[0], textPosition[1], textPaint);
}
public void setAnimationDirection(PINItemAnimator.ItemAnimationDirection animationDirection) {
this.animationDirection = animationDirection;
}
public void onAnimationUpdate(float percentCompleted) {
this.currentRadius = (int) (intendedRadius * percentCompleted);
this.textPaint.setAlpha((int) (255 * percentCompleted));
}
public boolean isAnimatedOut() {
return animationDirection == PINItemAnimator.ItemAnimationDirection.OUT;
}
}
@@ -0,0 +1,169 @@
package com.guardanis.applock.services;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.utils.CipherGenerator;
import javax.crypto.Cipher;
import ru.vtosters.lite.utils.AndroidUtils;
public class FingerprintLockService extends LockService {
private static final String PREF_ENROLLMENT_ALLOWED = "pin__fingerprint_enrollment_allowed";
protected CancellationSignal fingerprintCancellationSignal;
@Override
public boolean isEnrollmentEligible(Context context) {
return Build.VERSION_CODES.M <= Build.VERSION.SDK_INT && context.getResources().getBoolean(AndroidUtils.getIdentifier("applock__fingerprint_service_enabled", "bool")) && isHardwarePresent(context);
}
public void enroll(Context context, AuthenticationDelegate delegate) {
authenticate(context, false, delegate);
}
public void authenticate(Context context, AuthenticationDelegate delegate) {
authenticate(context, true, delegate);
}
protected void authenticate(Context context, boolean localEnrollmentRequired, AuthenticationDelegate delegate) {
int errorCode = getRequiredResolutionErrorCode(context, localEnrollmentRequired);
if (-1 < errorCode) {
delegate.onResolutionRequired(errorCode);
return;
}
// Should be handled by getRequiredResolutionErrorCode(Context, boolean)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
attemptFingerprintManagerAuthentication(context, delegate);
}
/**
* @return the resolvable error code or -1 if there are no issues requiring a resolution
*/
protected int getRequiredResolutionErrorCode(Context context, boolean localEnrollmentRequired) {
FingerprintManagerCompat manager = FingerprintManagerCompat.from(context);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return AppLock.ERROR_CODE_SDK_VERSION_MINIMUM;
if (localEnrollmentRequired && !isEnrolled(context))
return AppLock.ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED;
if (!isHardwarePresent(context)) return AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE;
if (!manager.hasEnrolledFingerprints()) return AppLock.ERROR_CODE_FINGERPRINTS_EMPTY;
if (ContextCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED)
return AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED;
return -1;
}
protected void attemptFingerprintManagerAuthentication(final Context context, final AuthenticationDelegate delegate) {
this.fingerprintCancellationSignal = new CancellationSignal();
var callback = new FingerprintManagerCompat.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
super.onAuthenticationError(errMsgId, errString);
delegate.onResolutionRequired(AndroidUtils.getIdentifier("applock__fingerprint_error_unknown", "string"));
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
super.onAuthenticationHelp(helpMsgId, helpString);
delegate.onAuthenticationHelp(helpMsgId, helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
notifyEnrolled(context);
delegate.onAuthenticationSuccess(result);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
};
delegate.onAuthenticating(fingerprintCancellationSignal);
try {
Cipher cipher = generateAuthCipher(context, false, 0);
FingerprintManagerCompat.CryptoObject cryptoObject = new FingerprintManagerCompat.CryptoObject(cipher);
FingerprintManagerCompat manager = FingerprintManagerCompat.from(context);
manager.authenticate(cryptoObject, 0, fingerprintCancellationSignal, callback, null);
} catch (Exception e) {
e.printStackTrace();
delegate.onResolutionRequired(AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE);
}
}
public boolean isHardwarePresent(Context context) {
return FingerprintManagerCompat.from(context).isHardwareDetected();
}
@Override
public boolean isEnrolled(Context context) {
return AppLock.getInstance(context).getPreferences().getBoolean(PREF_ENROLLMENT_ALLOWED, false);
}
protected void notifyEnrolled(Context context) {
AppLock.getInstance(context).getPreferences().edit().putBoolean(PREF_ENROLLMENT_ALLOWED, true).commit();
}
@SuppressLint("NewApi")
protected Cipher generateAuthCipher(Context context, boolean forceRegenerate, int attempts) throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
throw new RuntimeException("generateAuthCipher() not supported before Api 23");
return new CipherGenerator().generateAuthCipher(context, forceRegenerate, attempts);
}
@Override
public void invalidateEnrollments(Context context) {
AppLock.getInstance(context).getPreferences().edit().putBoolean(PREF_ENROLLMENT_ALLOWED, false).commit();
}
@Override
public void cancelPendingAuthentications(Context context) {
if (fingerprintCancellationSignal != null) {
this.fingerprintCancellationSignal.cancel();
this.fingerprintCancellationSignal = null;
}
}
public interface AuthenticationDelegate {
void onResolutionRequired(int errorCode);
void onAuthenticationHelp(int code, CharSequence message);
void onAuthenticating(CancellationSignal cancellationSignal);
void onAuthenticationSuccess(FingerprintManagerCompat.AuthenticationResult result);
void onAuthenticationFailed(String message);
}
}
@@ -0,0 +1,14 @@
package com.guardanis.applock.services;
import android.content.Context;
public abstract class LockService {
public abstract boolean isEnrolled(Context context);
public abstract boolean isEnrollmentEligible(Context context);
public abstract void cancelPendingAuthentications(Context context);
public abstract void invalidateEnrollments(Context context);
}
@@ -0,0 +1,74 @@
package com.guardanis.applock.services;
import android.content.Context;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.utils.CryptoUtils;
public class PINLockService extends LockService {
private static final String PREF_SAVED_LOCKED_PASSWORD = "pin__saved_locked_password";
@Override
public boolean isEnrollmentEligible(Context context) {
// I couldn't find a good reason to allow the disabling of the PIN as an option.
return true;
}
public void authenticate(Context context, String pin, AuthenticationDelegate eventListener) {
if (!isEnrolled(context)) {
eventListener.onNoPIN();
return;
}
if (!getEnrolledPIN(context).equals(CryptoUtils.encryptSha1(pin))) {
eventListener.onPINDoesNotMatch();
return;
}
eventListener.onPINMatches();
}
@Override
public boolean isEnrolled(Context context) {
return AppLock.getInstance(context)
.getPreferences()
.getString(PREF_SAVED_LOCKED_PASSWORD, null) != null;
}
private String getEnrolledPIN(Context context) {
return AppLock.getInstance(context)
.getPreferences()
.getString(PREF_SAVED_LOCKED_PASSWORD, null);
}
public void enroll(Context context, String pin) {
AppLock.getInstance(context)
.getPreferences()
.edit()
.putString(PREF_SAVED_LOCKED_PASSWORD, CryptoUtils.encryptSha1(pin))
.commit();
}
@Override
public void invalidateEnrollments(Context context) {
AppLock.getInstance(context)
.getPreferences()
.edit()
.remove(PREF_SAVED_LOCKED_PASSWORD)
.commit();
}
@Override
public void cancelPendingAuthentications(Context context) {
// There are none
}
public interface AuthenticationDelegate {
void onNoPIN();
void onPINDoesNotMatch();
void onPINMatches();
}
}
@@ -0,0 +1,69 @@
package com.guardanis.applock.utils;
import android.content.Context;
import android.os.Build;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import androidx.annotation.RequiresApi;
import java.security.KeyStore;
import java.security.UnrecoverableKeyException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import ru.vtosters.lite.utils.AndroidUtils;
@RequiresApi(api = Build.VERSION_CODES.M)
public class CipherGenerator {
private static final String KEYSTORE_NAME = "AndroidKeyStore";
public Cipher generateAuthCipher(Context context, boolean forceRegenerate, int attempts) throws Exception {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
throw new RuntimeException("generateAuthCipher() not supported before Api 23");
String alias = context.getString(AndroidUtils.getIdentifier("applock__fingerprint_alias", "string"));
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_NAME);
keyStore.load(null);
if (forceRegenerate || !keyStore.containsAlias(alias)) {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME);
keyGenerator.init(new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
}
String cipherFormat = String.format(
"%s/%s/%s",
KeyProperties.KEY_ALGORITHM_AES,
KeyProperties.BLOCK_MODE_CBC,
KeyProperties.ENCRYPTION_PADDING_PKCS7);
try {
Cipher cipher = Cipher.getInstance(cipherFormat);
cipher.init(Cipher.ENCRYPT_MODE, keyStore.getKey(alias, null));
return cipher;
} catch (KeyPermanentlyInvalidatedException e) {
e.printStackTrace();
if (1 < attempts)
return generateAuthCipher(context, true, attempts + 1);
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
if (1 < attempts)
return generateAuthCipher(context, true, attempts + 1);
}
return null;
}
}
@@ -0,0 +1,40 @@
package com.guardanis.applock.utils;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class CryptoUtils {
public static String encryptSha1(String text) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(text.getBytes(StandardCharsets.UTF_8), 0, text.length());
return convertToHex(md.digest());
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
private static String convertToHex(byte[] data) {
StringBuilder buf = new StringBuilder();
for (byte b : data) {
int half = (b >>> 4) & 0x0F;
int twoHalves = 0;
do {
buf.append(half <= 9
? (char) ('0' + half)
: (char) ('a' + half - 10));
half = b & 0x0F;
}
while (twoHalves++ < 1);
}
return buf.toString();
}
}
@@ -0,0 +1,85 @@
package com.guardanis.applock.utils;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import java.lang.ref.WeakReference;
public class LifeCycleUtils {
public static AppLockActivityLifeCycleCallbacks attach(Activity activity, AppLockActivityLifeCycleCallbacks.Delegate delegate) {
AppLockActivityLifeCycleCallbacks callbacks = new AppLockActivityLifeCycleCallbacks(activity, delegate);
activity.getApplication()
.registerActivityLifecycleCallbacks(callbacks);
return callbacks;
}
public static class AppLockActivityLifeCycleCallbacks implements Application.ActivityLifecycleCallbacks {
protected WeakReference<Activity> openedActivity;
protected WeakReference<Delegate> delegate;
public AppLockActivityLifeCycleCallbacks(Activity activity, Delegate delegate) {
this.openedActivity = new WeakReference<>(activity);
this.delegate = new WeakReference<>(delegate);
}
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
}
@Override
public void onActivityResumed(Activity activity) {
Activity opened = this.openedActivity.get();
Delegate delegate = this.delegate.get();
if (opened == null || delegate == null || opened != activity)
return;
delegate.onActivityResumed();
}
@Override
public void onActivityPaused(Activity activity) {
Activity opened = this.openedActivity.get();
Delegate delegate = this.delegate.get();
if (opened == null || delegate == null || opened != activity)
return;
delegate.onActivityPaused();
}
@Override
public void onActivityDestroyed(Activity activity) {
Activity opened = this.openedActivity.get();
if (opened == null || opened != activity)
return;
opened.getApplication()
.unregisterActivityLifecycleCallbacks(this);
}
public interface Delegate {
void onActivityResumed();
void onActivityPaused();
}
}
}
@@ -0,0 +1,184 @@
package com.guardanis.applock.views;
import android.Manifest;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.view.View;
import android.widget.TextView;
import androidx.core.app.ActivityCompat;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.pin.PINInputController;
import com.guardanis.applock.pin.PINInputView;
import com.guardanis.applock.utils.LifeCycleUtils;
import java.lang.ref.WeakReference;
import ru.vtosters.lite.utils.AndroidUtils;
public abstract class AppLockViewController implements LifeCycleUtils.AppLockActivityLifeCycleCallbacks.Delegate {
protected PINInputController pinInputController;
protected WeakReference<Activity> activity;
protected WeakReference<View> parent;
protected WeakReference<PINInputView> pinInputView;
protected WeakReference<TextView> descriptionView;
protected WeakReference<View> actionSettings;
protected boolean autoAuthorizationEnabled = true;
protected Application.ActivityLifecycleCallbacks activityLifecycleCallbacks;
public AppLockViewController(Activity activity, View parent) {
this.activity = new WeakReference<>(activity);
this.parent = new WeakReference<>(parent);
this.descriptionView = new WeakReference<>(parent.findViewById(AndroidUtils.getIdentifier("pin__description", "id")));
this.actionSettings = new WeakReference<>(parent.findViewById(AndroidUtils.getIdentifier("pin__action_settings", "id")));
this.pinInputView = new WeakReference<>(parent.findViewById(AndroidUtils.getIdentifier("pin__input_view", "id")));
int inputViewsCount = Integer.parseInt(AppLock.getPinCodeLimits(activity));
boolean passwordCharsEnabled = parent.getResources()
.getBoolean(AndroidUtils.getIdentifier("applock__item_password_chars_enabled", "bool"));
this.pinInputController = new PINInputController(pinInputView.get())
.setInputNumbersCount(inputViewsCount)
.setPasswordCharactersEnabled(passwordCharsEnabled);
this.activityLifecycleCallbacks = LifeCycleUtils.attach(activity, this);
}
public abstract void setupRootFlow();
public void setDescription(int descriptionResId) {
final TextView descriptionView = this.descriptionView.get();
if (descriptionView == null)
return;
descriptionView.setText(descriptionResId);
}
public void setDescription(String description) {
final TextView descriptionView = this.descriptionView.get();
if (descriptionView == null)
return;
descriptionView.setText(description);
}
protected <T extends View> void hide(WeakReference<T> weakView) {
final T view = weakView.get();
if (view == null)
return;
view.setVisibility(View.GONE);
}
protected <T extends View> void show(WeakReference<T> weakView) {
final T view = weakView.get();
if (view == null)
return;
view.setVisibility(View.VISIBLE);
}
public PINInputController getPINInputController() {
return pinInputController;
}
public View getParent() {
return parent.get();
}
public void setAutoAuthorizationEnabled(boolean autoAuthorizationEnabled) {
this.autoAuthorizationEnabled = autoAuthorizationEnabled;
}
public void updateActionSettings(final int errorCode) {
View actionSettings = this.actionSettings.get();
if (actionSettings == null)
return;
if (getSettingsIntent(errorCode) == null) {
actionSettings.setVisibility(View.GONE);
return;
}
actionSettings.setVisibility(View.VISIBLE);
actionSettings.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
handleActionSettingsClicked(errorCode);
}
});
}
protected abstract void handleActionSettingsClicked(int errorCode);
protected void handleInitialErrorPrompt(int errorCode) {
Activity activity = this.activity.get();
if (activity == null)
return;
if (errorCode == AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED) {
ActivityCompat.requestPermissions(
activity,
new String[]{Manifest.permission.USE_FINGERPRINT},
AppLock.REQUEST_CODE_FINGERPRINT_PERMISSION);
}
}
protected int getDescriptionResIdForError(int errorCode) {
return switch (errorCode) {
case AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED ->
AndroidUtils.getIdentifier("applock__fingerprint_error_permission", "string");
case AppLock.ERROR_CODE_FINGERPRINTS_EMPTY ->
AndroidUtils.getIdentifier("applock__fingerprint_error_none", "string");
case AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE ->
AndroidUtils.getIdentifier("applock__fingerprint_error_hardware", "string");
case AppLock.ERROR_CODE_FINGERPRINTS_NOT_LOCALLY_ENROLLED ->
AndroidUtils.getIdentifier("applock__fingerprint_error_not_enrolled", "string");
default -> AndroidUtils.getIdentifier("applock__fingerprint_error_unknown", "string");
};
}
/**
* @return an Intent directed towards the correct system setting for the error, or null if there is none.
*/
public Intent getSettingsIntent(int errorCode) {
return switch (errorCode) {
case AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED, AppLock.ERROR_CODE_FINGERPRINTS_EMPTY, AppLock.ERROR_CODE_FINGERPRINTS_MISSING_HARDWARE ->
new Intent(android.provider.Settings.ACTION_SECURITY_SETTINGS);
default -> null;
};
}
public void unregisterReceivers() {
Activity activity = this.activity.get();
if (activity == null)
return;
activity.getApplication()
.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
@Override
public void onActivityResumed() {
}
@Override
public void onActivityPaused() {
}
}
@@ -0,0 +1,253 @@
package com.guardanis.applock.views;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.view.View;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.core.os.CancellationSignal;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.pin.PINInputController;
import com.guardanis.applock.services.FingerprintLockService;
import com.guardanis.applock.services.PINLockService;
import java.lang.ref.WeakReference;
import ru.vtosters.lite.utils.AndroidUtils;
public class LockCreationViewController extends AppLockViewController
implements PINInputController.InputEventListener, FingerprintLockService.AuthenticationDelegate {
protected DisplayVariant displayVariant = DisplayVariant.NONE;
protected WeakReference<Delegate> delegate;
protected WeakReference<View> chooserParent;
protected String pinFirst;
public LockCreationViewController(Activity activity, View parent) {
super(activity, parent);
this.chooserParent = new WeakReference<>(parent.findViewById(AndroidUtils.getIdentifier("pin__create_chooser_items", "id")));
}
public LockCreationViewController setDelegate(Delegate delegate) {
this.delegate = new WeakReference<>(delegate);
return this;
}
@Override
public void setupRootFlow() {
Activity activity = this.activity.get();
if (activity == null)
return;
setupPINCreation();
}
protected void setupPINCreation() {
this.displayVariant = DisplayVariant.PIN_CREATION;
hide(chooserParent);
hide(actionSettings);
show(pinInputView);
setDescription(AndroidUtils.getIdentifier("applock__description_create_pin", "string"));
pinInputController.ensureKeyboardVisible();
pinInputController.setInputEventListener(this);
}
protected void setupPINConfirmation() {
this.displayVariant = DisplayVariant.PIN_CONFIRMATION;
hide(chooserParent);
hide(actionSettings);
show(pinInputView);
setDescription(AndroidUtils.getIdentifier("applock__description_confirm", "string"));
pinInputController.ensureKeyboardVisible();
pinInputController.setInputEventListener(this);
}
@Override
public void onInputEntered(String input) {
switch (displayVariant) {
case PIN_CREATION -> {
if (!pinInputController.matchesRequiredPINLength(input)) {
setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string"));
return;
}
this.pinFirst = input;
setupPINConfirmation();
}
case PIN_CONFIRMATION -> {
if (!pinInputController.matchesRequiredPINLength(input)) {
setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string"));
return;
}
if (!input.equals(pinFirst)) {
this.pinFirst = null;
setupPINCreation();
setDescription(AndroidUtils.getIdentifier("applock__description_create_pin_reattempt", "string"));
return;
}
createPINLock(input);
}
default -> {
}
}
}
protected void createPINLock(String input) {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.getLockService(PINLockService.class)
.enroll(activity, input);
var fingerprintLockService = AppLock.getInstance(activity).getLockService(FingerprintLockService.class);
if (fingerprintLockService.isEnrollmentEligible(activity)) {
setupFingerprintAuthentication();
} else {
handleLockCreated();
}
}
protected void setupFingerprintAuthentication() {
this.displayVariant = DisplayVariant.FINGERPRINT_AUTHENTICATION;
hide(pinInputView);
hide(chooserParent);
hide(actionSettings);
setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint", "string"));
if (autoAuthorizationEnabled)
attemptFingerprintAuthentication();
}
protected void attemptFingerprintAuthentication() {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.getLockService(FingerprintLockService.class)
.enroll(activity, this);
}
@Override
public void onResolutionRequired(int errorCode) {
setDescription(getDescriptionResIdForError(errorCode));
updateActionSettings(errorCode);
handleInitialErrorPrompt(errorCode);
}
@Override
public void onAuthenticationHelp(int code, CharSequence message) {
Activity activity = this.activity.get();
if (activity == null)
return;
String unformattedHelpMessage = activity.getString(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint_help", "string"));
String formatted = String.format(unformattedHelpMessage, message);
setDescription(formatted);
}
@Override
public void onAuthenticating(CancellationSignal cancellationSignal) {
}
@Override
public void onAuthenticationSuccess(FingerprintManagerCompat.AuthenticationResult result) {
handleLockCreated();
}
@Override
public void onAuthenticationFailed(String message) {
setDescription(message);
}
protected void handleLockCreated() {
this.displayVariant = DisplayVariant.NONE;
Delegate delegate = this.delegate.get();
if (delegate != null)
delegate.onLockCreated();
}
@Override
public void onActivityPaused() {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.cancelPendingAuthentications();
if (displayVariant == DisplayVariant.FINGERPRINT_AUTHENTICATION)
setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint_paused", "string"));
}
@Override
public void onActivityResumed() {
Activity activity = this.activity.get();
if (activity == null || displayVariant != DisplayVariant.FINGERPRINT_AUTHENTICATION)
return;
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
setDescription(AndroidUtils.getIdentifier("applock__fingerprint_error_permission_multiple", "string"));
updateActionSettings(AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED);
return;
}
setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint", "string"));
hide(actionSettings);
attemptFingerprintAuthentication();
}
@Override
protected void handleActionSettingsClicked(int errorCode) {
Activity activity = this.activity.get();
Intent intent = getSettingsIntent(errorCode);
if (activity == null || intent == null)
return;
activity.startActivity(intent);
}
public enum DisplayVariant {
NONE,
PIN_CREATION,
PIN_CONFIRMATION,
FINGERPRINT_AUTHENTICATION
}
public interface Delegate {
void onLockCreated();
}
}
@@ -0,0 +1,186 @@
package com.guardanis.applock.views;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.view.View;
import androidx.core.content.ContextCompat;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.pin.PINInputController;
import com.guardanis.applock.services.FingerprintLockService;
import java.lang.ref.WeakReference;
import ru.vtosters.lite.utils.AndroidUtils;
public class UnlockViewController extends AppLockViewController implements AppLock.UnlockDelegate, PINInputController.InputEventListener {
protected DisplayVariant displayVariant = DisplayVariant.PIN_UNLOCK;
protected WeakReference<Delegate> delegate;
public UnlockViewController(Activity activity, View parent) {
super(activity, parent);
}
public UnlockViewController setDelegate(Delegate delegate) {
this.delegate = new WeakReference<Delegate>(delegate);
return this;
}
@Override
public void setupRootFlow() {
View parent = this.parent.get();
if (parent == null)
return;
setupPINUnlock();
}
protected void setupPINUnlock() {
this.displayVariant = DisplayVariant.PIN_UNLOCK;
hide(actionSettings);
show(pinInputView);
setDescription(AndroidUtils.getIdentifier("applock__description_unlock_pin", "string"));
requestOpenKeyboard();
pinInputController.setInputEventListener(this);
FingerprintLockService fingerprintService = AppLock.getInstance(this.parent.get().getContext())
.getLockService(FingerprintLockService.class);
if (fingerprintService.isEnrolled(parent.get().getContext()))
attemptFingerprintAuthentication();
}
@Override
public void onInputEntered(String input) {
if (!pinInputController.matchesRequiredPINLength(input)) {
setDescription(AndroidUtils.getIdentifier("applock__unlock_error_insufficient_selection", "string"));
return;
}
attemptPINUnlock(input);
}
protected void attemptPINUnlock(String input) {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.attemptPINUnlock(input, this);
}
protected void attemptFingerprintAuthentication() {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.attemptFingerprintUnlock(this);
}
@Override
public void onUnlockSuccessful() {
this.displayVariant = DisplayVariant.NONE;
Delegate delegate = this.delegate.get();
if (delegate != null)
delegate.onUnlockSuccessful();
}
@Override
public void onResolutionRequired(int errorCode) {
setDescription(getDescriptionResIdForError(errorCode));
updateActionSettings(errorCode);
handleInitialErrorPrompt(errorCode);
}
@Override
public void onAuthenticationHelp(int code, String message) {
Activity activity = this.activity.get();
if (activity == null)
return;
String unformattedHelpMessage = activity.getString(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint_help", "string"));
String formatted = String.format(unformattedHelpMessage, message);
setDescription(formatted);
}
@Override
public void onFailureLimitExceeded(String message) {
setDescription(message);
}
@Override
public void requestOpenKeyboard() {
pinInputController.ensureKeyboardVisible();
}
@Override
public void onActivityPaused() {
Activity activity = this.activity.get();
if (activity == null)
return;
AppLock.getInstance(activity)
.cancelPendingAuthentications();
if (displayVariant == DisplayVariant.FINGERPRINT_AUTHENTICATION)
setDescription(AndroidUtils.getIdentifier("applock__description_create_fingerprint_paused", "string"));
}
@Override
public void onActivityResumed() {
Activity activity = this.activity.get();
if (activity == null || displayVariant != DisplayVariant.FINGERPRINT_AUTHENTICATION)
return;
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
setDescription(AndroidUtils.getIdentifier("applock__fingerprint_error_permission_multiple", "string"));
updateActionSettings(AppLock.ERROR_CODE_FINGERPRINTS_PERMISSION_REQUIRED);
return;
}
setDescription(AndroidUtils.getIdentifier("applock__description_unlock_fingerprint", "string"));
hide(actionSettings);
attemptFingerprintAuthentication();
}
@Override
protected void handleActionSettingsClicked(int errorCode) {
Activity activity = this.activity.get();
Intent intent = getSettingsIntent(errorCode);
if (activity == null || intent == null)
return;
activity.startActivity(intent);
}
public enum DisplayVariant {
NONE,
PIN_UNLOCK,
FINGERPRINT_AUTHENTICATION
}
public interface Delegate {
void onUnlockSuccessful();
}
}
@@ -1,17 +1,23 @@
package com.vk.api.audio;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.vk.api.base.ApiRequest;
import com.vk.dto.music.MusicTrack;
import com.vk.log.L;
import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.Genius;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.hooks.other.Preferences;
import java.util.ArrayList;
import ru.vtosters.lite.music.Genius;
import ru.vtosters.lite.utils.AndroidUtils;
public class AudioGetLyrics extends ApiRequest<AudioGetLyrics.a> {
private final MusicTrack musicTrack;
@@ -23,8 +23,8 @@ import com.vtosters.lite.R;
import com.vtosters.lite.ViewUtils;
import com.vtosters.lite.ui.MusicErrorViewHelper;
import com.vtosters.lite.ui.holder.RecyclerHolder;
import ru.vtosters.lite.music.Genius;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.Genius;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
+87 -6
View File
@@ -2140,6 +2140,8 @@ public final class R {
public static final int im_ic_msgdl = 0x7f040774; // 2130970484
public static final int im_ic_msgreload = 0x7f040775; // 2130970485
public static final int im_ic_stats = 0x7f040776; // 2130970486
public static final int pinBackgroundColor = 0x7f040777; // 2130970487
public static final int pinTextColor = 0x7f040778; // 2130970488
}
public static final class bool {
@@ -2160,6 +2162,9 @@ public final class R {
public static final int spb_default_reversed = 0x7f05000e; // 2131034126
public static final int vk_is_tablet = 0x7f05000f; // 2131034127
public static final int com_samsung_android_icon_container_has_icon_container = 0x7f050010; // 2131034128
public static final int applock__fingerprint_service_enabled = 0x7f050011; // 2131034129
public static final int applock__item_password_chars_enabled = 0x7f050012; // 2131034130
public static final int applock__unlock_activity_return_allowed = 0x7f050013; // 2131034131
}
public static final class color {
@@ -2968,6 +2973,13 @@ public final class R {
public static final int yellow_light = 0x7f060323; // 2131100451
public static final int yellow_sunflower = 0x7f060324; // 2131100452
public static final int iclauncher = 0x7f060325; // 2131100453
public static final int applock__action_settings = 0x7f060327; // 2131100455
public static final int applock__activity_background = 0x7f060328; // 2131100456
public static final int applock__create_choose_option_bg = 0x7f060329; // 2131100457
public static final int applock__create_choose_option_bg_pressed = 0x7f06032a; // 2131100458
public static final int applock__description_text = 0x7f06032b; // 2131100459
public static final int applock__item_background = 0x7f06032c; // 2131100460
public static final int applock__item_text = 0x7f06032d; // 2131100461
public static final int abc_color_highlight_material = 0x7f060004; // 2131099652
public static final int alpha05 = 0x7f060326; // 2131100454
}
@@ -3821,6 +3833,21 @@ public final class R {
public static final int wrap_content = 0x7f07035b; // 2131166043
public static final int year_label_height = 0x7f07035c; // 2131166044
public static final int year_label_text_size = 0x7f07035d; // 2131166045
public static final int applock__action_settings = 0x7f07035e; // 2131166046
public static final int applock__action_settings_margin_top = 0x7f07035f; // 2131166047
public static final int applock__action_settings_padding = 0x7f070360; // 2131166048
public static final int applock__activity_padding = 0x7f070361; // 2131166049
public static final int applock__chooser_image_size = 0x7f070362; // 2131166050
public static final int applock__chooser_parent_padding_vertical = 0x7f070363; // 2131166051
public static final int applock__chooser_text_padding_top = 0x7f070364; // 2131166052
public static final int applock__content_padding_horizontal = 0x7f070365; // 2131166053
public static final int applock__content_padding_vertical = 0x7f070366; // 2131166054
public static final int applock__description_padding_bottom = 0x7f070367; // 2131166055
public static final int applock__description_text = 0x7f070368; // 2131166056
public static final int applock__fingerprint_scan_image_margin_top = 0x7f070369; // 2131166057
public static final int applock__fingerprint_scan_image_size = 0x7f07036a; // 2131166058
public static final int applock__item_margin_top = 0x7f07036b; // 2131166059
public static final int applock__item_size = 0x7f07036c; // 2131166060
public static final int vkim_article_blocked_bottom_margin = 0x7f070329; // 2131165993
public static final int vkim_article_blocked_ic_margin = 0x7f07032a; // 2131165994
public static final int vkim_article_blocked_ic_size = 0x7f07032b; // 2131165995
@@ -4999,6 +5026,9 @@ public final class R {
public static final int custom_thumb_selector = 0x7f080be9; // 2131233769
public static final int custom_track_selector = 0x7f080bea; // 2131233770
public static final int splash = 0x7f080bec; // 2131233772
public static final int applock__chooser_option_bg = 0x7f080bee; // 2131233774
public static final int applock__fingerprint = 0x7f080bef; // 2131233775
public static final int applock__pin = 0x7f080bf0; // 2131233776
public static final int abc_control_background_material = 0x7f080032; // 2131230770
public static final int ic_action_close = 0x7f080322; // 2131231522
public static final int ic_camera_outline_28_new = 0x7f0803a0; // 2131231648
@@ -11062,6 +11092,12 @@ public final class R {
public static final int color_preview_border = 0x7f0a1024; // 2131365924
public static final int account_backup_view = 0x7f0a1025; // 2131365925
public static final int copy_image = 0x7f0a1026; // 2131365926
public static final int applock__activity_lock_creation = 0x7f0a1027; // 2131365927
public static final int applock__activity_unlock = 0x7f0a1028; // 2131365928
public static final int pin__action_settings = 0x7f0a1029; // 2131365929
public static final int pin__container = 0x7f0a102a; // 2131365930
public static final int pin__description = 0x7f0a102b; // 2131365931
public static final int pin__input_view = 0x7f0a102c; // 2131365932
}
public static final class integer {
@@ -11108,6 +11144,10 @@ public final class R {
public static final int status_bar_notification_info_maxnum = 0x7f0b002b; // 2131427371
public static final int vkim_activity_animation_duration = 0x7f0b002c; // 2131427372
public static final int vkim_playing_drawable_rect_count = 0x7f0b002d; // 2131427373
public static final int applock__activity_lock_reenable_minutes = 0x7f0b002e; // 2131427374
public static final int applock__failure_retry_delay = 0x7f0b002f; // 2131427375
public static final int applock__input_pin_item_count = 0x7f0b0030; // 2131427376
public static final int applock__max_retry_count = 0x7f0b0031; // 2131427377
public static final int emoji_size_in_sprite = 0x7f0b0012; // 2131427346
public static final int s1dip = 0x7f0b0027; // 2131427367
public static final int s2dip = 0x7f0b0028; // 2131427368
@@ -12883,6 +12923,11 @@ public final class R {
public static final int wppreview = 0x7f0d06df; // 2131560159
public static final int write_bar_i = 0x7f0d06e0; // 2131560160
public static final int color_picker_item = 0x7f0d06e1; // 2131560161
public static final int applock__activity_lock_creation = 0x7f0d06e2; // 2131560162
public static final int applock__activity_unlock = 0x7f0d06e3; // 2131560163
public static final int applock__fake_edit_text = 0x7f0d06e4; // 2131560164
public static final int applock__lock_creation = 0x7f0d06e5; // 2131560165
public static final int applock__unlock = 0x7f0d06e6; // 2131560166
}
public static final class menu {
@@ -19658,12 +19703,43 @@ public final class R {
public static final int vkx_integration_enabled_info = 0x7f12188a; // 2131892362
public static final int whitelisted_ad_groups_stories_title = 0x7f12188b; // 2131892363
public static final int writebar_ios_title = 0x7f12188c; // 2131892364
public static final int cache_size = 0x7f12188d; // 2131892365
public static final int cache_size_summ = 0x7f12188e; // 2131892366
public static final int clear_all_cache = 0x7f12188f; // 2131892367
public static final int invalidate_theme_cache_each_update_title = 0x7f121890; // 2131892368
public static final int invertFilters = 0x7f121891; // 2131892369
public static final int invertFilters_summ = 0x7f121892; // 2131892370
public static final int applock__action_settings = 0x7f12188d; // 2131892365
public static final int applock__create_chooser_option_fingerprint = 0x7f12188e; // 2131892366
public static final int applock__create_chooser_option_pin = 0x7f12188f; // 2131892367
public static final int applock__description_chooser = 0x7f121890; // 2131892368
public static final int applock__description_confirm = 0x7f121891; // 2131892369
public static final int applock__description_create_fingerprint = 0x7f121892; // 2131892370
public static final int applock__description_create_fingerprint_paused = 0x7f121893; // 2131892371
public static final int applock__description_create_pin = 0x7f121894; // 2131892372
public static final int applock__description_create_pin_reattempt = 0x7f121895; // 2131892373
public static final int applock__description_unlock_fingerprint = 0x7f121896; // 2131892374
public static final int applock__description_unlock_fingerprint_help = 0x7f121897; // 2131892375
public static final int applock__description_unlock_pin = 0x7f121898; // 2131892376
public static final int applock__empty_item_min_size_percent = 0x7f121899; // 2131892377
public static final int applock__fingerprint_alias = 0x7f12189a; // 2131892378
public static final int applock__fingerprint_error_hardware = 0x7f12189b; // 2131892379
public static final int applock__fingerprint_error_none = 0x7f12189c; // 2131892380
public static final int applock__fingerprint_error_not_enrolled = 0x7f12189d; // 2131892381
public static final int applock__fingerprint_error_permission = 0x7f12189e; // 2131892382
public static final int applock__fingerprint_error_permission_multiple = 0x7f12189f; // 2131892383
public static final int applock__fingerprint_error_unknown = 0x7f1218a0; // 2131892384
public static final int applock__fingerprint_error_unrecognized = 0x7f1218a1; // 2131892385
public static final int applock__password_char = 0x7f1218a2; // 2131892386
public static final int applock__toast_unlock_required = 0x7f1218a3; // 2131892387
public static final int applock__unlock_error_insufficient_selection = 0x7f1218a4; // 2131892388
public static final int applock__unlock_error_match_failed = 0x7f1218a5; // 2131892389
public static final int applock__unlock_error_no_matching_pin_found = 0x7f1218a6; // 2131892390
public static final int applock__unlock_error_retry_limit_exceeded = 0x7f1218a7; // 2131892391
public static final int cache_size = 0x7f1218a8; // 2131892392
public static final int cache_size_summ = 0x7f1218a9; // 2131892393
public static final int clear_all_cache = 0x7f1218aa; // 2131892394
public static final int invalidate_theme_cache_each_update_title = 0x7f1218ab; // 2131892395
public static final int invertFilters = 0x7f1218ac; // 2131892396
public static final int invertFilters_summ = 0x7f1218ad; // 2131892397
public static final int invertcached_summ = 0x7f1218ae; // 2131892398
public static final int invertcached_title = 0x7f1218af; // 2131892399
public static final int pin_not_set = 0x7f1218b0; // 2131892400
public static final int pin_reset = 0x7f1218b1; // 2131892401
}
public static final class style {
@@ -20799,6 +20875,11 @@ public final class R {
public static final int VkAmoledTheme = 0x7f130472; // 2131952754
public static final int VkMilkAmoledStyle = 0x7f130473; // 2131952755
public static final int VkMilkAmoledTheme = 0x7f130474; // 2131952756
public static final int applock__action_settings = 0x7f130487; // 2131952775
public static final int applock__content_parent = 0x7f130488; // 2131952776
public static final int applock__description = 0x7f130489; // 2131952777
public static final int applock__input_fingerprint_view = 0x7f13048a; // 2131952778
public static final int applock__input_pin_view = 0x7f13048b; // 2131952779
public static final int Base_V23_Theme_AppCompat = 0x7f13009d; // 2131951773
public static final int Base_V23_Theme_AppCompat_Light = 0x7f13009e; // 2131951774
public static final int Platform_V25_AppCompat = 0x7f13017f; // 2131951999
@@ -2,7 +2,7 @@ package ru.vtosters.hooks;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.catalog.inject.CatalogJsonInjector;
import ru.vtosters.hooks.music.injectors.CatalogJsonInjector;
public class CatalogsHook {
public static JSONObject injectCatalogGetAudio(JSONObject json) {
@@ -6,11 +6,12 @@ import android.content.Context;
import android.os.Build;
import com.google.firebase.iid.FirebaseInstanceIdReceiver;
import com.google.firebase.iid.FirebaseInstanceIdReceiver2;
import com.google.firebase.iid.FirebaseInstanceIdReceiver3;
import com.vtosters.lite.R;
import ru.vtosters.lite.utils.AndroidUtils;
public class GmsHook {
private static final boolean needToSpoof = !isGmsInstalled() && isFakeGmsInstalled();
private static final boolean needToSpoof = !isGmsInstalled() && (isFakeGmsInstalled() || isFakeGms2Installed());
public static boolean isGmsInstalled() {
try {
@@ -30,8 +31,17 @@ public class GmsHook {
}
} // Microg Google Market Services check
public static boolean isFakeGms2Installed() {
try {
AndroidUtils.getGlobalContext().getPackageManager().getPackageInfo("app.revanced.android.gms", 0);
return true;
} catch (Exception unused) {
return false;
}
} // Microg Google Market Services check
public static boolean isAnyServicesInstalled() {
return isGmsInstalled() || isFakeGmsInstalled();
return isGmsInstalled() || isFakeGmsInstalled() || isFakeGms2Installed();
}
public static void fixGapps() {
@@ -45,7 +55,7 @@ public class GmsHook {
} // Music channels fix
public static String replaceGMSPackage(String str) {
return needToSpoof ? str.replaceAll("com.google", "com.mgoogle") : str;
return needToSpoof ? str.replaceAll("com.google", (isFakeGms2Installed() ? "app.revanced" : "com.mgoogle")) : str;
}
public static String getFirebaseInstanceIdReceiver() {
@@ -53,6 +63,6 @@ public class GmsHook {
}
public static Class getFirebaseInstanceIdReceiverClass() {
return needToSpoof ? FirebaseInstanceIdReceiver2.class : FirebaseInstanceIdReceiver.class;
return needToSpoof ? (isFakeGms2Installed() ? FirebaseInstanceIdReceiver3.class : FirebaseInstanceIdReceiver2.class) : FirebaseInstanceIdReceiver.class;
}
}
@@ -4,6 +4,9 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import b.h.g.k.VKProgressDialog;
import com.aefyr.tsg.g2.TelegramStickersService;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.activities.UnlockActivity;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vtosters.lite.R;
import ru.vtosters.hooks.other.Preferences;
@@ -13,7 +16,6 @@ import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.ssfs.UsersList;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.ui.dialogs.DisableBattery;
import ru.vtosters.lite.ui.dialogs.InstallGMS;
import ru.vtosters.lite.ui.dialogs.OTADialog;
import ru.vtosters.lite.ui.dialogs.Start;
@@ -47,9 +49,8 @@ public class MainActivityInjector {
}
if (Preferences.isNewBuild()
&&!ThemesUtils.isMonetTheme()
&&ThemesManager.canApplyCustomAccent())
{
&& !ThemesUtils.isMonetTheme()
&& ThemesManager.canApplyCustomAccent()) {
Preferences.updateBuildNumber();
updateBinsAndTmpArchive(activity);
}
@@ -63,8 +64,16 @@ public class MainActivityInjector {
Start.alert(activity);
InstallGMS.alert(activity);
DisableBattery.alert(activity);
// VKIDProtection.alert(activity);
//needs to show selected tgs pack count in settings after cold launch
TelegramStickersService.getInstance(activity);
}
public static void applock(Activity activity) {
if (AppLock.isUnlockRequired(activity)) {
var intent = new Intent(activity, UnlockActivity.class);
activity.startActivity(intent);
}
}
private static void updateBinsAndTmpArchive(Activity activity) {
@@ -1,16 +0,0 @@
package ru.vtosters.hooks;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import java.io.File;
public class MusicCacheFilesHook {
public static File getTrackFile(String trackId) {
return new File(FileCacheImplementation.getTrackFolder(trackId), "track.mp3");
}
public static boolean isTrackExist(String trackId) {
return CacheDatabaseDelegate.isCached(trackId);
}
}
@@ -23,7 +23,7 @@ public class VKUIHook {
}
private static void applyVKUIStyles(WebView webView) {
if (getBoolValue("VKUI_INJ", true) && !webView.getUrl().contains("static.vk.com/memories")) {
if (getBoolValue("VKUI_INJ", true) && webView != null && webView.getUrl() != null && !webView.getUrl().contains("static.vk.com/memories")) {
if (!WebViewColoringUtils.isLoaded) {
WebViewColoringUtils.load();
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.hook;
package ru.vtosters.hooks.music;
import android.content.Context;
import com.vk.music.fragment.MusicFragment;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.hook;
package ru.vtosters.hooks.music;
import android.os.RemoteException;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
@@ -8,45 +8,29 @@ import com.vk.music.bottomsheets.a.MusicAction;
import com.vk.music.common.MusicPlaybackLaunchContext;
import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.VKXUtils;
import java.util.ArrayList;
import static bruhcollective.itaysonlab.libvkx.client.LibVKXClient.*;
import static ru.vtosters.hooks.other.Preferences.milkshake;
import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isCached;
import static ru.vtosters.lite.music.cache.CacheDatabaseDelegate.isVkxCached;
import static ru.vtosters.lite.utils.NetworkUtils.isNetworkConnected;
public class MusicBottomSheetHook {
public static ArrayList<MusicAction> hook(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
public static ArrayList<MusicAction> hook(final ArrayList<MusicAction> actions, final MusicTrack musicTrack) {
if (musicTrack.F1()) return actions;
var trackid = asId(musicTrack);
if (isVkxInstalled()) {
actions.add(getPlayInVKXAction());
}
final var trackId = asId(musicTrack);
if (isVkxInstalled()) actions.add(getPlayInVKXAction());
if (isIntegrationEnabled()) {
if (isVkxCached(trackid)) {
actions.add(getRemoveCacheTrackVkxAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction());
}
if (VKXUtils.isVkxCached(trackId)) actions.add(getRemoveCacheTrackVkxAction());
else if (isNetworkConnected()) actions.add(addToCacheTrackVkxAction());
} else {
if (isCached(trackid)) {
actions.add(getRemoveCacheTrackAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackAction());
}
if (MusicCacheImpl.isCachedTrack(trackId)) actions.add(getRemoveCacheTrackAction());
else if (isNetworkConnected()) actions.add(addToCacheTrackAction());
}
if (isNetworkConnected()) {
actions.add(downloadAsMp3Action());
}
if (isNetworkConnected()) actions.add(downloadAsMp3Action());
return actions;
}
@@ -54,9 +38,7 @@ public class MusicBottomSheetHook {
public static ArrayList<MusicAction> hookDownloadBTN(ArrayList<MusicAction> actions, MusicTrack musicTrack) {
if (musicTrack.F1()) return actions;
var trackid = asId(musicTrack);
if (isCached(trackid)) {
if (MusicCacheImpl.isCachedTrack(musicTrack.y1())) {
actions.add(new MusicAction(
R.id.music_action_toggle_download,
milkshake() ? R.drawable.ic_delete_outline_28 : R.drawable.ic_delete_24,
@@ -85,7 +67,7 @@ public class MusicBottomSheetHook {
if (isVkxInstalled()) actions.add(getPlayInVKXAction());
if (isIntegrationEnabled()) {
if (isVkxCached(playlist.a, playlist.b)) {
if (VKXUtils.isVkxCached(playlist.a, playlist.b)) {
actions.add(getRemoveCacheTrackVkxAction());
} else if (isNetworkConnected()) {
actions.add(addToCacheTrackVkxAction());
@@ -124,7 +106,7 @@ public class MusicBottomSheetHook {
}
if (actionId == R.id.remove_from_cache) {
CacheDatabaseDelegate.removeTrackFromCache(asId(track));
MusicCacheImpl.removeTrack(asId(track));
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
return true;
} else if (actionId == R.id.add_to_cache) {
@@ -0,0 +1,16 @@
package ru.vtosters.hooks.music;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
public class MusicCacheFilesHook {
public static File getTrackFile(String trackId) {
return MusicCacheStorageUtils.getTrackFile(trackId);
}
public static boolean isTrackExist(String trackId) {
return MusicCacheImpl.isCachedTrack(trackId);
}
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.hook;
package ru.vtosters.hooks.music;
import android.content.Context;
import androidx.annotation.AttrRes;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.catalog.inject;
package ru.vtosters.hooks.music.injectors;
import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
@@ -12,7 +12,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AccountManagerUtils;
import ru.vtosters.lite.utils.AndroidUtils;
@@ -48,7 +48,7 @@ public class CatalogJsonInjector {
setDefaultAudioPage(oldItems, catalog);
}
if (CacheDatabaseDelegate.hasTracks() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
var noPlaylists = !json.has("playlists");
if (noPlaylists) {
@@ -91,7 +91,7 @@ public class CatalogJsonInjector {
fixDailyMix(blocks);
if (!isUsersCatalog || !CacheDatabaseDelegate.hasTracks() || LibVKXClient.isIntegrationEnabled()) {
if (!isUsersCatalog || MusicCacheImpl.isEmpty() || LibVKXClient.isIntegrationEnabled()) {
return; // early return if not users catalog or no tracks or integration enabled
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache.injectors;
package ru.vtosters.hooks.music.injectors;
import android.os.RemoteException;
import android.text.TextUtils;
@@ -12,7 +12,7 @@ import io.reactivex.ObservableOnSubscribe;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
@@ -30,7 +30,7 @@ public class PlaylistInjector {
}
public static boolean eligibleForOfflineCaching() {
return CacheDatabaseDelegate.hasTracks();
return !MusicCacheImpl.isEmpty();
}
public static Observable<AudioGetPlaylist.c> injectGetPlaylist(AudioGetPlaylist audioGetPlaylist) {
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache.injectors;
package ru.vtosters.hooks.music.injectors;
import com.vk.dto.music.MusicTrack;
import com.vk.music.model.IpcPlayerModelImpl;
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music.cache.injectors;
package ru.vtosters.hooks.music.injectors;
import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
@@ -11,7 +11,7 @@ import io.reactivex.ObservableOnSubscribe;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.cache.helpers.PlaylistHelper;
import ru.vtosters.lite.music.cache.helpers.TracklistHelper;
import ru.vtosters.lite.utils.AccountManagerUtils;
@@ -56,7 +56,7 @@ public class TracklistInjector {
}
});
} else {
if (!CacheDatabaseDelegate.hasTracks()) {
if (MusicCacheImpl.isEmpty()) {
observableEmitter.b(parser.c(getEmptyCatalog()));
} else {
observableEmitter.b(parser.c(createVirtualCatalog(TracklistHelper.getTracks())));
@@ -135,7 +135,7 @@ public class TracklistInjector {
button.put("action", action);
button.put("block_id", "shuffle");
button.put("ref_items_count", CacheDatabaseDelegate.getTrackCount());
button.put("ref_items_count", MusicCacheImpl.getTracksCount());
button.put("ref_layout_name", "list");
button.put("ref_data_type", "music_audios");
@@ -158,7 +158,7 @@ public class TracklistInjector {
JSONArray blocks = firstSection.getJSONArray("blocks");
if (CacheDatabaseDelegate.hasTracks() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
if (!MusicCacheImpl.isEmpty() && !LibVKXClient.isIntegrationEnabled()) { // inj in playlist list
boolean noPlaylists = hasNoPlaylists(blocks);
if (noPlaylists) {
@@ -2,10 +2,13 @@ package ru.vtosters.hooks.other;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.StrictMode;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.activities.UnlockActivity;
import com.vk.medianative.MediaImageEncoder;
import com.vk.medianative.MediaNative;
import com.vtosters.lite.data.Users;
@@ -33,7 +36,15 @@ public class Preferences {
VTVerifications.load(application);
LifecycleUtils.registerActivities(application);
if (AppLock.isEnrolled(AndroidUtils.getGlobalContext())) {
AppLock.setAuthenticationRequired(AndroidUtils.getGlobalContext());
}
AnalyticsHelper.start(application);
if (AppLock.isEnrolled(AndroidUtils.getGlobalContext())) {
AppLock.setAuthenticationRequired(AndroidUtils.getGlobalContext());
}
} // VK Init
public static String getBuildName() {
@@ -263,7 +274,7 @@ public class Preferences {
}
public static boolean stories() {
return getBoolValue("stories", true);
return getBoolValue("stories", false);
}
public static boolean swipe() {
@@ -292,12 +303,12 @@ public class Preferences {
}
public static boolean isNewBuild() {
if(Preferences.getBoolValue("invalidate_theme_cache_each_update",true))
if (Preferences.getBoolValue("invalidate_theme_cache_each_update", true))
try {
return AndroidUtils.getGlobalContext()
.getPackageManager()
.getPackageInfo(AndroidUtils.getPackageName(), 0)
.lastUpdateTime != getPreferences().getLong("setupTime", 0L);
.getPackageManager()
.getPackageInfo(AndroidUtils.getPackageName(), 0)
.lastUpdateTime != getPreferences().getLong("setupTime", 0L);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
@@ -6,8 +6,8 @@ import com.vk.core.util.ToastUtils;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Playlist;
import com.vtosters.lite.R;
import ru.vtosters.hooks.MusicCacheFilesHook;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.hooks.music.MusicCacheFilesHook;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.callback.MusicCallbackBuilder;
import ru.vtosters.lite.music.converter.playlist.PlaylistConverter;
import ru.vtosters.lite.music.downloader.AudioGet;
@@ -58,10 +58,10 @@ public class AudioDownloader {
}
public static void cacheTrack(MusicTrack track) {
var trackId = track.y1();
var trackId = LibVKXClient.asId(track);
if (CacheDatabaseDelegate.isCached(trackId)) {
CacheDatabaseDelegate.removeTrackFromCache(LibVKXClient.asId(track));
if (MusicCacheImpl.isCachedTrack(trackId)) {
MusicCacheImpl.removeTrack(trackId);
AndroidUtils.sendToast(AndroidUtils.getString("audio_deleted_from_cache"));
return;
}
@@ -9,6 +9,8 @@ import okhttp3.Request;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.vtosters.lite.R;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.utils.AndroidUtils;
@@ -7,14 +7,13 @@ import androidx.annotation.Nullable;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R;
import okhttp3.Callback;
import okhttp3.*;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AndroidUtils;
import java.io.IOException;
@@ -47,7 +46,7 @@ public class LastFMScrobbler {
return;
}
if (Preferences.autocache() && !CacheDatabaseDelegate.isCached(uid)) {
if (Preferences.autocache() && !MusicCacheImpl.isCachedTrack(uid)) {
if (LibVKXClient.isIntegrationEnabled()) {
LibVKXClient.getInstance().runOnService(service -> {
try {
@@ -1,158 +0,0 @@
package ru.vtosters.lite.music;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import java8.util.concurrent.CompletableFuture;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import ru.vtosters.lite.di.singleton.VtOkHttpClient;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.converter.ts.FFMpeg;
import ru.vtosters.lite.music.converter.ts.TSMerger;
import ru.vtosters.lite.music.downloader.ThumbnailDownloader;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import javax.crypto.NoSuchPaddingException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class M3UDownloader implements ITrackDownloader {
private static final OkHttpClient client = VtOkHttpClient.getInstance();
public static M3UDownloader getInstance() {
return Holder.INSTANCE;
}
public static String getTitle(MusicTrack track) {
String title = null;
if (track.f != null) {
title = track.f;
if (!track.g.isEmpty()) {
title += " (" + track.g + ")";
}
}
return title;
}
public void downloadTrack(MusicTrack track, File outDir, Callback callback, boolean cache) {
if (track.D.isEmpty()) {
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
callback.onFailure();
return;
}
try {
Request request = new Request.a().b(track.D).a();
String response = client.a(request).execute().a().g();
parse(response, outDir, callback, track, cache);
} catch (IOException e) {
e.printStackTrace();
callback.onFailure();
}
}
private void parse(String payload, File outDir, Callback callback, MusicTrack track, boolean cache) throws UnsupportedEncodingException {
if (cache) downloadThumbnails(track);
VKM3UParser parser = new VKM3UParser(track.D.substring(0, track.D.lastIndexOf("/") + 1), payload);
AtomicInteger progress = new AtomicInteger(0);
List<CompletableFuture<Void>> futures = createFutures(parser, progress, callback, track);
var resultTs = getResultTsFile(track);
var resultMp3 = getResultMp3File(outDir, cache, track);
callback.onProgress(5);
var future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
future.join();
future.exceptionally(throwable -> {
callback.onFailure();
return null;
}).thenApply(v -> TSMerger.merge(getTsesDir(track), resultTs))
.thenApply(mergeResult -> mergeResult && FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track))
.thenApply(convertResult -> {
if (convertResult)
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / parser.mTransportStreams.size()));
else callback.onFailure();
return convertResult;
}).thenRun(() -> IOUtils.deleteRecursive(getTsesDir(track))).thenRun(() -> {
callback.onSuccess();
if (cache) CacheDatabaseDelegate.insertTrack(track);
});
}
private void downloadThumbnails(MusicTrack track) {
try {
ThumbnailDownloader.downloadThumbnails(track);
} catch (IOException e) {
e.printStackTrace();
}
}
private List<CompletableFuture<Void>> createFutures(VKM3UParser parser, AtomicInteger progress, Callback callback, MusicTrack track) {
List<CompletableFuture<Void>> futures = new ArrayList<>(parser.mTransportStreams.size());
var tses = parser.mTransportStreams;
for (var i = 0; i < tses.size(); ++i) {
final var ts = tses.get(i);
final var filename = i + ".ts";
futures.add(CompletableFuture.runAsync(() -> processTransportStream(parser, ts, filename, progress, tses.size(), callback, track)));
}
return futures;
}
private void processTransportStream(VKM3UParser parser, TransportStream ts, String filename, AtomicInteger progress, int size, Callback callback, MusicTrack track) {
try {
InputStream mediaIs = IOUtils.openStream(ts.getMediaSegmentUri());
byte[] content;
if (ts.needDecoding()) {
InputStream keyPubIs = IOUtils.openStream(ts.getKeyURL());
content = IOUtils.decodeStream(mediaIs, IOUtils.readFully(keyPubIs));
keyPubIs.close();
} else {
content = IOUtils.readFully(mediaIs);
}
File tsDump = new File(getTsesDir(track), filename);
IOUtils.writeToFile(tsDump, content);
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
callback.onSizeReceived((long) content.length * size, parser.mHeapSize);
mediaIs.close();
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException |
InvalidAlgorithmParameterException | InvalidKeyException e) {
callback.onFailure();
throw new RuntimeException(e);
}
}
private File getTsesDir(MusicTrack track) {
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
var tsesDir = new File(filesDir, String.valueOf(track.d));
tsesDir.mkdirs();
return tsesDir;
}
private File getResultTsFile(MusicTrack track) {
return new File(getTsesDir(track), "result.ts");
}
private File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
var fileName = IOUtils.getValidFileName((cache ? "track" : track.C + " - " + getTitle(track)) + ".mp3");
return new File(outDir, fileName);
}
// Initialization-on-demand
private static class Holder {
private static final M3UDownloader INSTANCE = new M3UDownloader();
}
}
@@ -1,36 +0,0 @@
package ru.vtosters.lite.music;
import android.text.TextUtils;
public class TransportStream {
private final String mMediaSegmentUri;
private String mKeyUri;
private String mMediaSegmentName;
public TransportStream(String baseURI, String mediaSegmentName) {
this.mMediaSegmentUri = baseURI + mediaSegmentName;
}
public TransportStream(String keyURI, String baseURI, String mediaSegmentName) {
this.mKeyUri = keyURI;
this.mMediaSegmentUri = baseURI + mediaSegmentName;
this.mMediaSegmentName = mediaSegmentName;
}
public boolean needDecoding() {
return !TextUtils.isEmpty(mKeyUri);
}
public String getKeyURL() {
return mKeyUri;
}
public String getMediaSegmentUri() {
return this.mMediaSegmentUri;
}
public String getMediaSegmentName() {
return this.mMediaSegmentName;
}
}
@@ -1,126 +0,0 @@
package ru.vtosters.lite.music;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* The parser for extended M3U playlist format used by VK.
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#ref-M3U">M3U</a>
**/
public class VKM3UParser {
//Playlist types
static public int PLAYLIST_TYPE_VOD = 0;
static public int PLAYLIST_TYPE_EVENT = 1;
private final String mData;
//region Extended M3U global extensions
public List<TransportStream> mTransportStreams = new ArrayList<>();
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.1.2">EXT-X-VERSION</a>
**/
public int mVersion = -1;
/**
* The EXT-X-ALLOW-CACHE tag is used in protocol version 6 or lower.
* If the protocol version is upgraded by VK, we will not support that.
**/
public boolean mAllowCache = false;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1">EXT-X-TARGETDURATION</a>
**/
public int mTargetDuration = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.2">EXT-X-MEDIA-SEQUENCE</a>
**/
public int mMediaSequence = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.3">EXT-X-DISCONTINUITY-SEQUENCE</a>
**/
public int mDiscontinuitySequence = -1;
//endregion
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.5">EXT-X-PLAYLIST-TYPE</a>
**/
public int mPlaylistType = -1;
/**
* @see <a href="https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.6">EXT-X-I-FRAMES-ONLY</a>
**/
public boolean mIFramesOnly = false;
public long mHeapSize = 0;
public VKM3UParser(String baseUri, String data) {
mData = data;
init(baseUri);
}
public static boolean isTS(String str) {
return str.matches(".+\\.ts.*");
}
private void init(String baseUri) {
if (TextUtils.isEmpty(mData)) throw new NullPointerException("mData==null");
Scanner scanner = new Scanner(mData);
String line = scanner.nextLine();
if (!"#EXTM3U".equals(line))
throw new IllegalStateException(String.format("Unknown initial M3U tag: %s", line));
boolean aes128 = false;
String keyUri = "";
while (scanner.hasNextLine()) {
line = scanner.nextLine();
if ('#' == line.charAt(0)) {
parseTag(line);
if (line.startsWith("#EXT-X-KEY:") && line.contains("METHOD=AES-128")) {
aes128 = true;
keyUri = getKeyUri(line, baseUri);
}
} else if (isTS(line)) {
addTransportStream(aes128, keyUri, baseUri, line);
aes128 = false;
} else {
throw new RuntimeException(String.format("Failed to parse: %s", line));
}
mHeapSize += line.getBytes().length;
}
}
private void parseTag(String line) {
if (line.startsWith("#EXT-X-VERSION:"))
mVersion = Integer.parseInt(line.substring(15));
else if (line.startsWith("#EXT-X-ALLOW-CACHE:"))
mAllowCache = "YES".equalsIgnoreCase(line.substring(19));
else if (line.startsWith("#EXT-X-TARGETDURATION:"))
mTargetDuration = Integer.parseInt(line.substring(22));
else if (line.startsWith("#EXT-X-MEDIA-SEQUENCE:"))
mMediaSequence = Integer.parseInt(line.substring(22));
else if (line.startsWith("#EXT-X-DISCONTINUITY-SEQUENCE:"))
mDiscontinuitySequence = Integer.parseInt(line.substring(30));
else if (line.startsWith("#EXT-X-PLAYLIST-TYPE:"))
parsePlaylistType(line.substring(21));
else if (line.startsWith("#EXT-X-I-FRAMES-ONLY"))
mIFramesOnly = true;
}
private void parsePlaylistType(String type) {
switch (type) {
case "VOD" -> mPlaylistType = PLAYLIST_TYPE_VOD;
case "EVENT" -> mPlaylistType = PLAYLIST_TYPE_EVENT;
default -> throw new IllegalStateException(String.format("Unknown playlist type: %s", type));
}
}
private String getKeyUri(String line, String baseUri) {
String substr = line.substring(11);
substr = substr.substring(substr.indexOf('"') + 1, substr.lastIndexOf('"'));
//TODO: supposed endpoint
return substr.startsWith("http") ? substr : baseUri + substr;
}
private void addTransportStream(boolean aes128, String keyUri, String baseUri, String line) {
TransportStream ts = aes128
? new TransportStream(keyUri, baseUri, line)
: new TransportStream(baseUri, line);
mTransportStreams.add(ts);
}
}
@@ -1,343 +0,0 @@
package ru.vtosters.lite.music.cache;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
import com.vk.core.util.ToastUtils;
import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R;
import org.json.JSONObject;
import ru.vtosters.lite.utils.AndroidUtils;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import static ru.vtosters.lite.music.cache.FileCacheImplementation.getCacheDir;
import static ru.vtosters.lite.utils.AndroidUtils.getGlobalContext;
import static ru.vtosters.lite.utils.AndroidUtils.sendToast;
import static ru.vtosters.lite.utils.IOUtils.deleteRecursive;
public class CacheDatabaseDelegate {
private static final int DB_VERSION = 0x1;
private static final String DB_NAME = "vt_lite_cache.db";
private static final String TABLE_NAME = "tracks";
private static final String COLUMN_ID = "id";
private static final String COLUMN_TRACK_ID = "trackId";
private static final String COLUMN_ALBUM_ID = "albumId";
private static final String COLUMN_TITLE = "title";
private static final String COLUMN_SUBTITLE = "subtitle";
private static final String COLUMN_ARTIST = "artist";
private static final String COLUMN_ALBUM_TITLE = "albumTitle";
private static final String COLUMN_EXPLICIT = "explicit";
private static final String COLUMN_HAS_ARTWORK = "hasArtwork";
private static final String COLUMN_DURATION = "duration";
private static DbHelper helperInstance;
protected static DbHelper getHelper() {
if (helperInstance == null) helperInstance = new DbHelper(getGlobalContext());
return helperInstance;
}
// You MUST call this method ONLY AFTER TRACK IS DOWNLOADED!
public static void insertTrack(MusicTrack track) {
try {
var json = track.J();
var albumNode = json.optJSONObject("album");
var hasArtwork = false;
var albumId = -1;
var albumName = "";
if (albumNode != null) {
hasArtwork = albumNode.has("thumb");
albumId = albumNode.getInt("id");
albumName = albumNode.getString("title");
}
var formula = String.format(
"INSERT INTO %s (%s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES ('%s', %s, '%s', '%s', '%s', '%s', %s, %s, %s)",
TABLE_NAME,
COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK,
track.y1(),
albumId,
URLEncoder.encode(json.getString("title"), "UTF-8"),
URLEncoder.encode(json.optString("subtitle"), "UTF-8"),
URLEncoder.encode(json.getString("artist"), "UTF-8"),
URLEncoder.encode(albumName, "UTF-8"),
json.optInt("is_explicit"),
json.getInt("duration"),
hasArtwork ? 1 : 0
);
getHelper().getReadableDatabase().execSQL(formula);
} catch (Exception e) {
e.printStackTrace();
ToastUtils.a(AndroidUtils.getString(R.string.track_bd_insert_err) + " [" + e.getMessage() + "]", true);
}
}
public static void insertTracks(List<MusicTrack> tracks) {
for (var track : tracks) insertTrack(track);
}
public static void clear() {
getHelper().getReadableDatabase().execSQL(String.format("DELETE FROM %s", TABLE_NAME));
deleteRecursive(getCacheDir());
sendToast(AndroidUtils.getString(R.string.cached_tracks_deleted));
}
@NonNull
private static JSONObject reconstructFromDb(Cursor cursor) {
try {
var trackId = cursor.getString(getColumnIndex(cursor, COLUMN_TRACK_ID));
var splits = trackId.split("_");
var id = Integer.parseInt(splits[1]);
var ownerId = Integer.parseInt(splits[0]);
return new JSONObject()
.put("url", FileCacheImplementation.getServerPath())
.put("id", id)
.put("owner_id", ownerId)
.put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_TITLE)), "UTF-8"))
.put("subtitle", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_SUBTITLE)), "UTF-8"))
.put("artist", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ARTIST)), "UTF-8"))
.put("duration", cursor.getInt(getColumnIndex(cursor, COLUMN_DURATION)))
.put("is_explicit", cursor.getInt(getColumnIndex(cursor, COLUMN_EXPLICIT)))
.put("access_key", 2281488)
.putOpt("album", reconstructAlbumFromDb(cursor, trackId));
} catch (Exception e) {
e.printStackTrace();
return new JSONObject();
}
}
@Nullable
private static JSONObject reconstructAlbumFromDb(Cursor cursor, String trackId) {
try {
if (!FileCacheImplementation.getTrackThumbnail(trackId, 300).exists()
&& !FileCacheImplementation.getTrackThumbnail(trackId, 600).exists()
&& !FileCacheImplementation.getTrackThumbnail(trackId, 1200).exists()
) {
var ownerId = Integer.parseInt(trackId.split("_")[1]);
return new JSONObject()
.put("id", cursor.getInt(getColumnIndex(cursor, COLUMN_ALBUM_ID)))
.put("owner_id", ownerId)
.put("title", URLDecoder.decode(cursor.getString(getColumnIndex(cursor, COLUMN_ALBUM_TITLE)), "UTF-8"))
.putOpt("thumb", reconstructAlbumThumbFromDb(cursor, trackId));
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Nullable
private static JSONObject reconstructAlbumThumbFromDb(Cursor cursor, String trackId) {
try {
if (cursor.getInt(getColumnIndex(cursor, COLUMN_HAS_ARTWORK)) != 0)
new JSONObject()
.put("height", 600)
.put("width", 600)
.putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300)))
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600)))
.putOpt("photo_1200", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 1200)));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static int getColumnIndex(Cursor cursor, String name) {
return cursor.getColumnIndexOrThrow(name);
}
public static boolean hasTracks() {
if (LibVKXClient.isIntegrationEnabled()) {
return true;
}
var formula = String.format(
"SELECT COUNT(%s) FROM %s",
COLUMN_ID,
TABLE_NAME
);
var cursor = getHelper().getReadableDatabase().rawQuery(formula, null);
if (cursor != null) {
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return count > 0;
}
return false;
}
public static List<MusicTrack> getTracks() {
return getTracks(String.format(
"SELECT * FROM %s",
TABLE_NAME
));
}
public static List<MusicTrack> getTracksAsPlaylist() {
return getTracks(String.format(
"SELECT * FROM %s WHERE %s > -1 GROUP BY %s HAVING COUNT(*) > 1 ORDER by id DESC",
TABLE_NAME,
COLUMN_ALBUM_ID,
COLUMN_ALBUM_ID
));
}
public static List<MusicTrack> getTracksByAlbum(String id) {
return getTracks(String.format(
"SELECT COUNT(%s) FROM %s WHERE %s = %s",
COLUMN_ID,
TABLE_NAME,
COLUMN_ALBUM_ID,
id
));
}
public static List<MusicTrack> getTracksByAlbumOne(String id) {
return getTracks(String.format(
"SELECT * FROM %s WHERE %s = %s LIMIT 1",
TABLE_NAME,
COLUMN_ALBUM_ID,
id
));
}
public static List<MusicTrack> getTracks(String query) {
List<MusicTrack> list = new ArrayList<>();
try (Cursor cursor = getHelper().getReadableDatabase().rawQuery(query, new String[0])) {
while (cursor.moveToNext()) {
list.add(new MusicTrack(reconstructFromDb(cursor)));
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
public static boolean isCached(String trackId) {
var formula = String.format(
"SELECT COUNT(%s) FROM %s WHERE %s = '%s'",
COLUMN_ID,
TABLE_NAME,
COLUMN_TRACK_ID,
trackId
);
try {
Cursor size = getHelper().getReadableDatabase().rawQuery(formula, null);
if (size != null) {
size.moveToFirst();
int count = size.getInt(0);
size.close();
return count != 0;
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
public static boolean isVkxCached(int playlist_id, int owner_id) {
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
@Override
public Boolean run(ILibVkxService service) {
try {
return service.isPlaylistCached(playlist_id, owner_id);
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
public static boolean isVkxCached(String trackId) {
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
@Override
public Boolean run(ILibVkxService service) {
try {
var splits = trackId.split("_");
var id = Integer.parseInt(splits[1]);
var owner = Integer.parseInt(splits[0]);
return service.isTrackCached(id, owner);
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
public static void removeTrackFromCache(String trackId) {
deleteRecursive(FileCacheImplementation.getTrackFolder(trackId));
var formula = String.format(
"DELETE FROM %s WHERE %s = '%s'",
TABLE_NAME,
COLUMN_TRACK_ID,
trackId
);
getHelper().getWritableDatabase().execSQL(formula);
}
public static long getTrackCount() {
if (!LibVKXClient.isIntegrationEnabled())
return DatabaseUtils.queryNumEntries(getHelper().getReadableDatabase(), TABLE_NAME);
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<Long>() {
@Override
public Long run(ILibVkxService service) {
try {
return (long) service.getCache().size();
} catch (RemoteException e) {
e.printStackTrace();
return defaultValue();
}
}
@Override
public Long defaultValue() {
return 0L;
}
});
}
private static class DbHelper extends SQLiteOpenHelper {
public DbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
public void onCreate(SQLiteDatabase db) {
var formula = String.format(
"CREATE TABLE %s (%s INTEGER PRIMARY KEY AUTOINCREMENT, %s TEXT, %s INTEGER, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s INTEGER, %s INTEGER, %s INTEGER)",
TABLE_NAME,
COLUMN_ID, COLUMN_TRACK_ID, COLUMN_ALBUM_ID, COLUMN_TITLE, COLUMN_SUBTITLE, COLUMN_ARTIST, COLUMN_ALBUM_TITLE, COLUMN_EXPLICIT, COLUMN_DURATION, COLUMN_HAS_ARTWORK
);
db.execSQL(formula);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
@@ -1,50 +0,0 @@
package ru.vtosters.lite.music.cache;
import android.net.Uri;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.hooks.MusicCacheFilesHook;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import java.io.File;
public class FileCacheImplementation {
public static void clear() {
CacheDatabaseDelegate.clear();
IOUtils.deleteRecursive(getCacheDir(), false);
}
public static File getCacheDir() {
return AndroidUtils.getGlobalContext().getExternalFilesDir("vt_tracks");
}
public static File getTrackFolder(String trackId) {
var dir = new File(getCacheDir(), trackId);
dir.mkdirs();
return dir;
}
public static File getThumbnailsFolder(String trackId) {
var dir = new File(getTrackFolder(trackId), "thumbs");
dir.mkdirs();
return dir;
}
public static File getTrackFile(MusicTrack track) {
return MusicCacheFilesHook.getTrackFile(track.y1());
}
public static File getTrackThumbnail(String trackId, int factor) {
return new File(getThumbnailsFolder(trackId), "thumb_" + factor + ".png");
}
public static String getFileUri(File file) {
if (!file.exists()) return null;
return Uri.fromFile(file).toString();
}
public static String getServerPath() {
return "https://vtosters.app";
}
}
@@ -0,0 +1,85 @@
package ru.vtosters.lite.music.cache;
import android.content.Context;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.cache.db.MusicCacheDb;
import java.util.List;
public class MusicCacheDbDelegate {
static synchronized private MusicCacheDb connectToDb(Context context) {
synchronized (MusicCacheDb.class) {
return new MusicCacheDb(context);
}
}
public static void addTrack(Context context,
String trackId,
String albumId,
String title,
String subtitle,
String artist,
String albumTitle,
boolean explicit,
int duration,
boolean hasArtwork) {
try (var db = connectToDb(context)) {
db.addTrack(
trackId,
albumId,
title,
subtitle,
artist,
albumTitle,
explicit,
duration,
hasArtwork);
}
}
public static void removeTrack(Context context, String trackId) {
try (var db = connectToDb(context)) {
db.deleteTrack(trackId);
}
}
public static List<MusicTrack> getAllTracks(Context context) {
try (var db = connectToDb(context)) {
return db.getAllTracks();
}
}
public static List<MusicTrack> getAlbumById(Context context, String albumId) {
try (var db = connectToDb(context)) {
return db.getAlbum(albumId);
}
}
public static List<MusicTrack> getFirstAlbumTrack(Context context, String albumId) {
try (var db = connectToDb(context)) {
return db.getFirstAlbumTrack(albumId);
}
}
public static List<MusicTrack> getPlaylist(Context context) {
try (var db = connectToDb(context)) {
return db.getPlaylist();
}
}
public static long getTracksCount(Context context) {
try (var db = connectToDb(context)) {
return db.getTracksCount();
}
}
public static boolean isCachedTrack(Context context, String trackId) {
try (var db = connectToDb(context)) {
return db.isCachedTrack(trackId);
}
}
public static void drop(Context context) {
context.deleteDatabase(MusicCacheDb.Constants.DB_NAME);
}
}
@@ -0,0 +1,83 @@
package ru.vtosters.lite.music.cache;
import android.os.RemoteException;
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.util.List;
public class MusicCacheImpl {
public static void addTrack(MusicTrack track) {
MusicCacheDbDelegate.addTrack(
AndroidUtils.getGlobalContext(),
track.y1(),
track.I != null ? track.I.getId() + "" : "-1",
track.f,
track.g,
track.C,
track.I != null ? track.I.getTitle() : "",
track.K,
track.h,
track.I != null && track.I.u1() != null);
}
public static void removeTrack(String trackId) {
MusicCacheDbDelegate.removeTrack(AndroidUtils.getGlobalContext(), trackId);
MusicCacheStorageUtils.removeTrackDirById(trackId);
}
public static List<MusicTrack> getAllTracks() {
return MusicCacheDbDelegate.getAllTracks(AndroidUtils.getGlobalContext());
}
public static List<MusicTrack> getAlbumById(String albumId) {
return MusicCacheDbDelegate.getAlbumById(AndroidUtils.getGlobalContext(), albumId);
}
public static List<MusicTrack> getFirstAlbumTrack(String albumId) {
return MusicCacheDbDelegate.getFirstAlbumTrack(AndroidUtils.getGlobalContext(), albumId);
}
public static List<MusicTrack> getPlaylist() {
return MusicCacheDbDelegate.getPlaylist(AndroidUtils.getGlobalContext());
}
public static long getTracksCount() {
return !LibVKXClient.isIntegrationEnabled()
? MusicCacheDbDelegate.getTracksCount(AndroidUtils.getGlobalContext())
: LibVKXClient.getInstance().runOnServiceSync(
new LibVKXClientImpl.LibVKXActionGeneric<Long>() {
@Override
public Long run(ILibVkxService service) {
try {
return (long) service.getCache().size();
} catch (RemoteException e) {
e.printStackTrace();
return defaultValue();
}
}
@Override
public Long defaultValue() {
return 0L;
}
});
}
public static boolean isCachedTrack(String trackId) {
return MusicCacheDbDelegate.isCachedTrack(AndroidUtils.getGlobalContext(), trackId);
}
public static boolean isEmpty() {
return !LibVKXClient.isIntegrationEnabled() && getTracksCount() == 0L;
}
public static void clear() {
MusicCacheDbDelegate.drop(AndroidUtils.getGlobalContext());
MusicCacheStorageUtils.clear();
}
}
@@ -0,0 +1,246 @@
package ru.vtosters.lite.music.cache.db;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;
import androidx.annotation.Nullable;
import com.vk.dto.music.AlbumLink;
import com.vk.dto.music.MusicTrack;
import com.vk.dto.music.Thumb;
import org.json.JSONObject;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
/**
* <p class="note"><strong>Note:</strong> the {@link AutoCloseable} interface was
* first added in the {@link android.os.Build.VERSION_CODES#Q} release for the {@link SQLiteOpenHelper} </p>
*/
public class MusicCacheDb extends SQLiteOpenHelper implements AutoCloseable { //for pre-Q
public MusicCacheDb(Context context) {
super(context, Constants.DB_NAME, null, Constants.DV_VERSION);
}
private static MusicTrack fromCursor(Cursor cur) {
try {
int trackIdIndex = cur.getColumnIndex(Constants.COLUMN_TRACK_ID);
int titleIndex = cur.getColumnIndex(Constants.COLUMN_TITLE);
int subtitleIndex = cur.getColumnIndex(Constants.COLUMN_SUBTITLE);
int artistIndex = cur.getColumnIndex(Constants.COLUMN_ARTIST);
int durationIndex = cur.getColumnIndex(Constants.COLUMN_DURATION);
int explicitIndex = cur.getColumnIndex(Constants.COLUMN_EXPLICIT);
int albumIdIndex = cur.getColumnIndex(Constants.COLUMN_ALBUM_ID);
int albumTitleIndex = cur.getColumnIndex(Constants.COLUMN_ALBUM_TITLE);
if (trackIdIndex == -1 || titleIndex == -1 || subtitleIndex == -1 || artistIndex == -1 || durationIndex == -1 || explicitIndex == -1 || albumIdIndex == -1) {
return null;
}
String trackId = cur.getString(trackIdIndex);
String[] splits = trackId.split("_");
if (splits.length != 2) {
return null;
}
MusicTrack res = new MusicTrack();
res.J = Integer.MAX_VALUE + ""; //access_key
res.D = "https://vtosters.app"; //url
res.d = Integer.parseInt(splits[1]); //id
res.e = Integer.parseInt(splits[0]); //owner_id
res.f = URLDecoder.decode(cur.getString(titleIndex), "UTF-8"); //title
res.g = URLDecoder.decode(cur.getString(subtitleIndex), "UTF-8"); //subtitle
res.C = URLDecoder.decode(cur.getString(artistIndex), "UTF-8"); //artist
res.h = cur.getInt(durationIndex); //duration
res.K = cur.getInt(explicitIndex) > 0; //is_explicit
int albumId = Integer.parseInt(cur.getString(albumIdIndex));
String albumTitle = albumTitleIndex != -1 ? cur.getString(albumTitleIndex) : null;
JSONObject node = new JSONObject();
File f300 = MusicCacheStorageUtils.getTrackThumb(trackId, 300);
File f600 = MusicCacheStorageUtils.getTrackThumb(trackId, 600);
File f1200 = MusicCacheStorageUtils.getTrackThumb(trackId, 1200);
if (f300.exists()) node.putOpt("photo_300", Uri.fromFile(f300).toString());
if (f600.exists()) node.putOpt("photo_600", Uri.fromFile(f600).toString());
if (f1200.exists()) node.putOpt("photo_1200", Uri.fromFile(f1200).toString());
res.I = new AlbumLink(
albumId,
res.e,
res.J,
albumTitle,
node.length() > 0
? new Thumb(node.put("width", 600)
.put("height", 600))
: null
); //album
return res;
} catch (Throwable ignored) {
// ignored
}
return null;
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(Constants.CREATE_QUERY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < newVersion) {
List<MusicTrack> tracks = getTracksWithCursor(db.query(Constants.TABLE_NAME, null, null, null, null, null, null));
db.execSQL(Constants.DROP_QUERY);
onCreate(db);
for (MusicTrack track : tracks) {
ContentValues vals = new ContentValues();
vals.put(Constants.COLUMN_TRACK_ID, track.y1());
vals.put(Constants.COLUMN_ALBUM_ID, track.I != null ? track.I.getId() + "" : "-1");
vals.put(Constants.COLUMN_TITLE, track.f);
vals.put(Constants.COLUMN_SUBTITLE, track.g);
vals.put(Constants.COLUMN_ARTIST, track.C);
vals.put(Constants.COLUMN_ALBUM_TITLE, track.I != null ? track.I.getTitle() : "");
vals.put(Constants.COLUMN_EXPLICIT, Boolean.compare(track.K, true));
vals.put(Constants.COLUMN_DURATION, track.h);
vals.put(Constants.COLUMN_HAS_ARTWORK, track.I != null && track.I.u1() != null);
db.insert(Constants.TABLE_NAME, null, vals);
}
}
}
public void addTrack(String trackId, String albumId, String title, String subtitle, String artist, String albumTitle, boolean explicit, int duration, boolean hasArtwork) {
ContentValues vals = new ContentValues();
vals.put(Constants.COLUMN_TRACK_ID, trackId);
vals.put(Constants.COLUMN_ALBUM_ID, albumId);
vals.put(Constants.COLUMN_TITLE, title);
vals.put(Constants.COLUMN_SUBTITLE, subtitle);
vals.put(Constants.COLUMN_ARTIST, artist);
vals.put(Constants.COLUMN_ALBUM_TITLE, albumTitle);
vals.put(Constants.COLUMN_EXPLICIT, Boolean.compare(explicit, true));
vals.put(Constants.COLUMN_DURATION, duration);
vals.put(Constants.COLUMN_HAS_ARTWORK, Boolean.compare(hasArtwork, true));
long row = getWritableDatabase().insert(Constants.TABLE_NAME, null, vals);
if (AndroidUtils.isDebuggable()) Log.d("MusicCacheDb", "addTrack(): " + row);
}
public void deleteTrack(String trackId) {
getWritableDatabase().delete(Constants.TABLE_NAME, Constants.COLUMN_TRACK_ID + " = ?", new String[]{trackId});
}
public boolean isCachedTrack(String trackId) {
try (var cur = getReadableDatabase().query(
Constants.TABLE_NAME,
new String[]{Constants.COLUMN_TRACK_ID},
Constants.COLUMN_TRACK_ID + " = ?",
new String[]{trackId},
null,
null,
null,
"0,1")) {
return cur.moveToFirst();
}
}
public long getTracksCount() {
return DatabaseUtils.queryNumEntries(getReadableDatabase(), Constants.TABLE_NAME);
}
public List<MusicTrack> getAllTracks() {
return getTracksWithCursor(getReadableDatabase().query(Constants.TABLE_NAME, null, null, null, null, null, null));
}
public List<MusicTrack> getAlbum(String albumId) {
return getAlbumTracks(albumId, null);
}
public List<MusicTrack> getFirstAlbumTrack(String albumId) {
return getAlbumTracks(albumId, "0,1");
}
private List<MusicTrack> getAlbumTracks(String albumId, @Nullable String limit) {
return getTracksWithCursor(getReadableDatabase().query(
Constants.TABLE_NAME,
null,
Constants.COLUMN_ALBUM_ID + " = ?",
new String[]{albumId},
null,
null,
null,
limit));
}
public List<MusicTrack> getPlaylist() {
return getTracksWithCursor(getWritableDatabase().query(
Constants.TABLE_NAME,
null,
Constants.COLUMN_ALBUM_ID + " > -1",
null,
Constants.COLUMN_ALBUM_ID,
"count(*) > 1",
Constants.COLUMN_ID + " desc"));
}
private List<MusicTrack> getTracksWithCursor(Cursor cur) {
List<MusicTrack> res = new ArrayList<>();
if (cur != null && cur.moveToFirst()) {
try (cur) {
do {
MusicTrack track = fromCursor(cur);
if (track != null) res.add(track);
} while (cur.moveToNext());
}
}
return res;
}
@Retention(RetentionPolicy.SOURCE)
public @interface Constants {
int DV_VERSION = 0x2;
String DB_NAME = "vt_lite_cache.db";
String TABLE_NAME = "tracks";
//columns
String COLUMN_ID = "id";
String COLUMN_TRACK_ID = "trackId";
String COLUMN_ALBUM_ID = "albumId";
String COLUMN_TITLE = "title";
String COLUMN_SUBTITLE = "subtitle";
String COLUMN_ARTIST = "artist";
String COLUMN_ALBUM_TITLE = "albumTitle";
String COLUMN_EXPLICIT = "explicit";
String COLUMN_DURATION = "duration";
String COLUMN_HAS_ARTWORK = "hasArtwork";
//queries
String CREATE_QUERY = "create table " + TABLE_NAME + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY autoincrement,"
+ COLUMN_TRACK_ID + " text not null,"
+ COLUMN_ALBUM_ID + " text not null,"
+ COLUMN_TITLE + " text not null,"
+ COLUMN_SUBTITLE + " text not null,"
+ COLUMN_ARTIST + " text not null,"
+ COLUMN_ALBUM_TITLE + " text not null,"
+ COLUMN_EXPLICIT + " int not null,"
+ COLUMN_DURATION + " int not null,"
+ COLUMN_HAS_ARTWORK + " int not null"
+ ")";
String DROP_QUERY = "drop table if exists " + TABLE_NAME;
}
}
@@ -1,13 +1,14 @@
package ru.vtosters.lite.music.cache.helpers;
import android.net.Uri;
import com.vk.dto.music.Playlist;
import com.vtosters.lite.R;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.util.ArrayList;
import java.util.List;
@@ -44,8 +45,8 @@ public class PlaylistHelper {
if (!photo.isEmpty())
jPhoto.put("height", 600)
.put("width", 600)
.putOpt("photo_300", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 300)))
.putOpt("photo_600", FileCacheImplementation.getFileUri(FileCacheImplementation.getTrackThumbnail(trackId, 600)));
.putOpt("photo_300", Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId, 300)).toString())
.putOpt("photo_600", Uri.fromFile(MusicCacheStorageUtils.getTrackThumb(trackId, 600)).toString());
var obj = new JSONObject()
.put("id", -2)
.put("owner_id", albumId)
@@ -68,7 +69,7 @@ public class PlaylistHelper {
public static List<Playlist> getAlbumPlaylists() {
List<Playlist> list = new ArrayList<>();
for (var track : CacheDatabaseDelegate.getTracksAsPlaylist()) {
for (var track : MusicCacheImpl.getPlaylist()) {
var albumLink = track.I;
var thumb = albumLink.u1();
list.add(createAlbum(
@@ -6,12 +6,14 @@ import com.vk.dto.music.MusicTrack;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TracklistHelper {
@@ -20,22 +22,33 @@ public class TracklistHelper {
}
public static List<MusicTrack> getTracks() {
return CacheDatabaseDelegate.getTracks();
List<MusicTrack> tracks = MusicCacheImpl.getAllTracks();
boolean doNotinvertOrder = Preferences.getBoolValue("invertCachedTracks", false);
if (!doNotinvertOrder) {
Collections.reverse(tracks);
}
return tracks;
}
public static List<MusicTrack> getTracks(String id) {
return CacheDatabaseDelegate.getTracksByAlbum(id);
return MusicCacheImpl.getAlbumById(id);
}
public static List<MusicTrack> getTracksOne(String id) {
return CacheDatabaseDelegate.getTracksByAlbumOne(id);
return MusicCacheImpl.getFirstAlbumTrack(id);
}
public static JSONArray tracksToIds(List<MusicTrack> tracks) {
var arr = new JSONArray();
for (MusicTrack track : tracks) {
arr.put(track.y1());
}
return arr;
}
@@ -43,7 +56,7 @@ public class TracklistHelper {
var arr = new JSONArray();
for (MusicTrack track : tracks) {
var json = track.J();
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track));
var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
try {
addCachedThumbnails(json, folder);
} catch (JSONException | MalformedURLException e) {
@@ -58,7 +71,7 @@ public class TracklistHelper {
List<MusicTrack> tracks = new ArrayList<>();
for (MusicTrack track : list) {
var json = track.J();
var folder = FileCacheImplementation.getThumbnailsFolder(LibVKXClient.asId(track));
var folder = MusicCacheStorageUtils.getThumbDirById(LibVKXClient.asId(track));
try {
addCachedThumbnails(json, folder);
} catch (JSONException | MalformedURLException e) {
@@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import com.vtosters.lite.R;
import ru.vtosters.lite.downloaders.notifications.NotificationChannels;
import ru.vtosters.lite.music.Callback;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.utils.AndroidUtils;
public class MusicCallbackBuilder {
@@ -1,23 +1,11 @@
package ru.vtosters.lite.music.converter.ts;
import android.util.Log;
import com.arthenica.ffmpegkit.FFmpegKit;
import com.arthenica.ffmpegkit.ReturnCode;
import com.vk.core.util.ToastUtils;
import com.vk.dto.music.Artist;
import com.vk.dto.music.MusicTrack;
import com.vtosters.lite.R;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.stream.Collectors;
public class FFMpeg {
private static final int BUFFER_SIZE = 2048;
/**
* @param in Merged .ts file via {@link ru.vtosters.lite.music.converter.ts.TSMerger}
@@ -25,79 +13,47 @@ public class FFMpeg {
* @param track Track to be converted
* @return True if success, else false
*/
public static boolean convert(File in, String out, MusicTrack track) {
public static void convert(File in, String out, MusicTrack track) throws Throwable {
// convert .ts file to .mp3 file via ffmpeg
var sb = new StringBuilder();
sb.append("-y -i \"").append(in.getAbsolutePath()).append("\"");
sb.append(" -map 0 -dn ");
//sb.append(" -loglevel error");
//sb.append(" -hide_banner");
sb.append(" -write_id3v2 1");
String title = null;
if (track.f != null) {
title = track.f;
if (!track.g.isEmpty()) {
title += " (" + track.g + ")";
}
}
if (title != null) {
sb.append(" -metadata title=\"").append(IOUtils.getValidFileName(title)).append("\"");
}
if (track.L != null) {
sb.append(" -metadata artist=\"").append(track.L.stream().map(Artist::w1).collect(Collectors.joining(", "))).append("\"");
} else if (track.C != null) {
sb.append(" -metadata artist=\"").append(track.C).append("\"");
}
if (track.I != null) {
sb.append(" -metadata album=\"").append(track.I.getTitle()).append("\"");
}
sb.append(" -c copy \"").append(out).append("\"");
var command = sb.toString();
var session = FFmpegKit.execute(command);
var rc = session.getReturnCode();
if (ReturnCode.isSuccess(rc)) {
return true;
}
Log.e("FFMpeg", "FFmpegKit failed with return code " + rc);
return false;
var cmd = " -y -i \"" + in.getAbsolutePath() + '"'
+ " -map 0 -dn"
+ " -loglevel error"
+ " -hide_banner"
+ " -write_id3v2 1"
// +" -metadata title=\""+IOUtils.getValidFileName(M3UDownloader.getTitle(track))+'"'
// +" -metadata artist=\""+normalizeMetadata(track.L.stream().map(Artist::w1).collect(Collectors.joining(", ")))+'"'
// +" -metadata album=\""+normalizeMetadata(track.I.getTitle())+'"'
+ " -c copy \"" + out + '"';
var ses = FFmpegKit.execute(cmd);
if (ses.getReturnCode().isValueCancel()) throw new RuntimeException(ses.getLogsAsString());
}
private static void copyAssets() {
try {
String[] files = AndroidUtils.getGlobalContext().getAssets().list("ffmpeg");
for (String file : files) {
if (file.endsWith(".so")) {
InputStream in = AndroidUtils.getGlobalContext().getAssets().open("ffmpeg" + File.separator + file);
FileOutputStream out = new FileOutputStream(AndroidUtils.getGlobalContext().getFilesDir() + File.separator + file);
byte[] buffer = new byte[BUFFER_SIZE];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
in.close();
out.close();
}
}
} catch (IOException e) {
e.printStackTrace();
}
private static String normalizeMetadata(String in) {
return in.replace('"', '‟');
}
private static void checkFFMpegLibs() {
var lib = new File(AndroidUtils.getGlobalContext().getFilesDir(), "libffmpegkit_abidetect.so");
if (!lib.exists()) {
ToastUtils.a(R.string.ffmpegkit_copy);
copyAssets();
}
}
// private static void copyAssets()
// throws IOException
// {
// try
// {
// for (var filename:AndroidUtils.getGlobalContext().getAssets().list("ffmpeg"))
// if(filename.endsWith(".so"))
// IOUtils.writeToFile(
// new File(AndroidUtils.getGlobalContext().getFilesDir(),filename),
// IOUtils.readFully(AndroidUtils.getGlobalContext().getAssets().open("ffmpeg"+File.separator+filename)));
// }
// catch(IOException e)
// { throw e; }
// }
//
// private static void checkFFMpegLibs()
// {
// var lib=new File(AndroidUtils.getGlobalContext().getFilesDir(),"libffmpegkit_abidetect.so");
// if(!lib.exists())
// {
// ToastUtils.a(R.string.ffmpegkit_copy);
// copyAssets();
// }
// }
}
@@ -9,66 +9,62 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.Comparator;
public class TSMerger {
/**
* Merge multiple .ts files into one .ts file
*
* @param in Directory with .ts files
* @return True if success, else false
*/
public static boolean merge(File in, File out) {
var tsesList = in.listFiles();
public static void merge(File in, File out) {
File[] tsesList = in.listFiles();
if (tsesList == null) {
return false;
return;
}
var mergedByteArrayStream = new ByteArrayOutputStream();
ByteArrayOutputStream mergedByteArrayStream = new ByteArrayOutputStream();
sortTsesList(tsesList);
for (var file : tsesList) {
for (File file : tsesList) {
if (file.getName().endsWith(".ts")) {
try {
mergedByteArrayStream.write(IOUtils.readFully(file));
} catch (IOException e) {
e.printStackTrace();
return false;
return;
}
}
}
return writeOutputFile(out, mergedByteArrayStream);
writeOutputFile(out, mergedByteArrayStream);
}
private static void sortTsesList(File[] tsesList) {
Arrays.sort(tsesList, (ts1, ts2) -> {
String ts1Name = ts1.getName();
String ts2Name = ts2.getName();
int i1 = Integer.parseInt(ts1Name.substring(0, ts1Name.indexOf('.', 1)));
int i2 = Integer.parseInt(ts2Name.substring(0, ts2Name.indexOf('.', 1)));
return Integer.compare(i1, i2);
});
Arrays.sort(tsesList, Comparator.comparingInt(TSMerger::getFileIndex));
}
private static boolean writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
private static int getFileIndex(File file) {
String fileName = file.getName();
int dotIndex = fileName.indexOf('.', 1);
return Integer.parseInt(fileName.substring(0, dotIndex));
}
private static void writeOutputFile(File out, ByteArrayOutputStream mergedByteArrayStream) {
byte[] bytes = mergedByteArrayStream.toByteArray();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
Files.write(out.toPath(), mergedByteArrayStream.toByteArray());
} catch (IOException e) {
e.printStackTrace();
return false;
Files.write(out.toPath(), bytes);
} catch (IOException ignored) {
// ignored
}
} else {
try (FileOutputStream fos = new FileOutputStream(out)) {
fos.write(mergedByteArrayStream.toByteArray());
} catch (IOException e) {
e.printStackTrace();
return false;
fos.write(bytes);
} catch (IOException ignored) {
// ignored
}
}
return true;
}
}
@@ -0,0 +1,177 @@
package ru.vtosters.lite.music.downloader;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.source.hls.playlist.e;
import com.google.android.exoplayer2.source.hls.playlist.f;
import com.google.android.exoplayer2.source.hls.playlist.f.a;
import com.vk.dto.music.MusicTrack;
import java8.util.concurrent.CompletableFuture;
import java8.util.concurrent.CompletionException;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.converter.ts.FFMpeg;
import ru.vtosters.lite.music.converter.ts.TSMerger;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class M3UDownloader
implements ITrackDownloader {
public static M3UDownloader getInstance() {
return Holder.INSTANCE;
}
static void parse(
File outDir,
Callback callback,
MusicTrack track,
boolean cache)
throws IOException {
var parser = new com.google.android.exoplayer2.source.hls.playlist.h(e.a(track.D));
var baseUri = track.D.substring(0, track.D.lastIndexOf("/") + 1);
var segments = ((f) parser.a(Uri.parse(baseUri), IOUtils.openStream(track.D))).o;
var progress = new AtomicInteger();
callback.onProgress(5);
var resultTs = getResultTsFile(track);
var resultMp3 = getResultMp3File(outDir, cache, track);
try {
CompletableFuture.allOf(createFutures(baseUri, segments, progress, callback, track))
.thenRun(() ->
{
if (cache)
try {
ThumbnailDownloader.downloadThumbnails(track);
} catch (IOException e) {
throw new RuntimeException("Failed to download thumbs", e);
}
})
.thenRun(() ->
{
try {
TSMerger.merge(getTsesDir(track), resultTs);
} catch (Throwable e) {
throw new RuntimeException("Failed to merge ts files", e);
}
})
.thenRun(() ->
{
try {
FFMpeg.convert(resultTs, resultMp3.getAbsolutePath(), track);
} catch (Throwable e) {
throw new RuntimeException("FFmpeg error", e);
}
})
.thenRun(() -> callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / segments.size())))
.thenRun(() -> MusicCacheImpl.addTrack(track))
.whenComplete((unused, tr) -> {
if (tr != null) {
IOUtils.deleteRecursive(MusicCacheStorageUtils.getTrackDirById(track.y1()));
callback.onFailure();
} else callback.onSuccess();
IOUtils.deleteRecursive(getTsesDir(track));
})
.join();
} catch (CompletionException e) {
Log.e("M3UDownloader", "Failed to download track", e.getCause());
}
}
@SuppressWarnings("rawtypes")
static CompletableFuture[] createFutures(
String baseUri,
List<a> segments,
AtomicInteger progress,
Callback callback,
MusicTrack track) {
var futures = new CompletableFuture[segments.size()];
for (var i = 0; i < segments.size(); ++i) {
var segment = segments.get(i);
var filename = i + ".ts";
futures[i] = CompletableFuture.runAsync(() -> processSegment(baseUri, segment, filename, progress, segments.size(), callback, track));
}
return futures;
}
private static void processSegment(
String baseUri,
a segment,
String filename,
AtomicInteger progress,
int size,
Callback callback,
MusicTrack track) {
try {
var buff = IOUtils.readFully(IOUtils.openStream(baseUri + segment.a/*url*/));
if (!TextUtils.isEmpty(segment.g)) {
var cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
var cipherKey = new SecretKeySpec(IOUtils.readFully(IOUtils.openStream(segment.g)), "AES");
var cipherBytes = segment.h.getBytes();
//if IV doesn't pass, it must be created manually and filled with zeros
if (cipherBytes.length != 16) Arrays.fill((cipherBytes = new byte[16]), (byte) 0x0);
var cipherIv = new IvParameterSpec(cipherBytes);
cipher.init(Cipher.DECRYPT_MODE, cipherKey, cipherIv);
buff = cipher.doFinal(buff);
}
IOUtils.writeToFile(new File(getTsesDir(track), filename), buff);
callback.onProgress(10 + Math.round(80.0f * progress.addAndGet(1) / size));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String getTitle(MusicTrack track) {
return track.f + (!TextUtils.isEmpty(track.g) ? +'(' + track.g + ')' : null);
}
static File getTsesDir(MusicTrack track) {
var filesDir = AndroidUtils.getGlobalContext().getFilesDir();
var tsesDir = new File(filesDir, String.valueOf(track.d));
tsesDir.mkdirs();
return tsesDir;
}
static File getResultTsFile(MusicTrack track) {
return new File(getTsesDir(track), "result.ts");
}
static File getResultMp3File(File outDir, boolean cache, MusicTrack track) {
return new File(outDir, IOUtils.getValidFileName((cache ? "track" : track.C + " - " + getTitle(track)) + ".mp3"));
}
@Override
public void downloadTrack(
MusicTrack track,
File outDir,
Callback callback,
boolean cache) {
if (track.D.isEmpty()) {
Log.d("TrackDownloader", "link error: " + track.y1() + ", title: " + M3UDownloader.getTitle(track));
callback.onFailure();
return;
}
try {
parse(outDir, callback, track, cache);
} catch (IOException e) {
callback.onFailure();
Log.e("M3UDownloader", e + "");
throw new RuntimeException(e);
}
}
// Initialization-on-demand
private static class Holder {
private static final M3UDownloader INSTANCE = new M3UDownloader();
}
}
@@ -70,7 +70,7 @@
// }
//
// @Override
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.Callback callback) {
// public void downloadTrack(String url, File outDir, ru.vtosters.lite.music.interfaces.Callback callback) {
//
// }
//}
@@ -2,7 +2,7 @@ package ru.vtosters.lite.music.downloader;
import android.util.Log;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.Callback;
import ru.vtosters.lite.music.interfaces.Callback;
import java.io.File;
import java.util.List;
@@ -10,16 +10,12 @@ import java.util.concurrent.atomic.AtomicInteger;
public class PlaylistDownloader {
public static void downloadPlaylist(List<MusicTrack> playlist, String playlistName, String path, Callback callback) {
File outDir = new File(path);
var outDir = new File(path);
if (!outDir.exists())
if (outDir.mkdirs())
Log.v("PlaylistDownloader", "Directory created");
else
Log.e("PlaylistDownloader", "Directory creation failed");
var downloadedTracks = new AtomicInteger(0);
for (MusicTrack musicTrack : playlist) {
if (outDir.mkdirs()) Log.v("PlaylistDownloader", "Directory created");
else Log.e("PlaylistDownloader", "Directory creation failed");
var downloadedTracks = new AtomicInteger();
for (var musicTrack : playlist)
TrackDownloader.downloadTrack(musicTrack, path, new Callback() {
@Override
public void onProgress(int progress) {
@@ -27,8 +23,7 @@ public class PlaylistDownloader {
@Override
public void onSuccess() {
var currentProgress = downloadedTracks.incrementAndGet();
callback.onProgress(currentProgress);
callback.onProgress(downloadedTracks.incrementAndGet());
}
@Override
@@ -40,13 +35,11 @@ public class PlaylistDownloader {
public void onSizeReceived(long size, long header) {
}
});
}
}
public static void cachePlaylist(List<MusicTrack> playlist, Callback callback) {
var downloadedTracks = new AtomicInteger(0);
for (MusicTrack musicTrack : playlist) {
var downloadedTracks = new AtomicInteger();
for (var musicTrack : playlist)
TrackDownloader.cacheTrack(musicTrack, new Callback() {
@Override
public void onProgress(int progress) {
@@ -54,8 +47,7 @@ public class PlaylistDownloader {
@Override
public void onSuccess() {
var currentProgress = downloadedTracks.incrementAndGet();
callback.onProgress(currentProgress);
callback.onProgress(downloadedTracks.incrementAndGet());
}
@Override
@@ -67,6 +59,5 @@ public class PlaylistDownloader {
public void onSizeReceived(long size, long header) {
}
});
}
}
}
@@ -2,45 +2,42 @@ package ru.vtosters.lite.music.downloader;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import org.json.JSONArray;
import org.json.JSONObject;
import ru.vtosters.lite.utils.IOUtils;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.IOException;
import java.net.URL;
public class ThumbnailDownloader {
public static void downloadThumbnails(MusicTrack track)
throws IOException {
var obj = track.J();
var trackId = LibVKXClient.asId(track);
if (!obj.has("album")) return;
var album = obj.optJSONObject("album");
if (!album.has("thumb")) return;
var thumb = album.optJSONObject("thumb");
if (thumb.has("sizes")) {
var sizes = thumb.optJSONArray("sizes");
for (int i = 0; i < sizes.length(); i++) {
var size = sizes.optJSONObject(i);
var width = size.optInt("width");
var src = size.optString("src");
downloadThumbnail(src, width, trackId);
public static void downloadThumbnails(MusicTrack track) throws IOException {
JSONObject json = track.J();
JSONObject album = json.optJSONObject("album");
if (album == null) return;
JSONObject thumb = album.optJSONObject("thumb");
if (thumb == null) return;
JSONArray sizes = thumb.optJSONArray("sizes");
String trackId = LibVKXClient.asId(track);
if (sizes == null) {
JSONArray names = thumb.names();
if (names == null) return;
for (int i = 0; i < names.length(); ++i) {
String name = names.optString(i);
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId);
}
} else {
var names = thumb.names();
for (int i = 0; i < names.length(); i++) {
var name = names.optString(i);
downloadThumbnail(thumb.optString(name), Integer.parseInt(name.substring(6)), trackId);
for (int i = 0; i < sizes.length(); ++i) {
JSONObject size = sizes.optJSONObject(i);
int width = size.optInt("width");
String src = size.optString("src");
downloadThumbnail(src, width, trackId);
}
}
}
private static void downloadThumbnail(String url, int res, String trackId)
throws IOException {
var data = IOUtils.readFully(new URL(url).openStream());
IOUtils.writeToFile(FileCacheImplementation.getTrackThumbnail(trackId, res), data);
private static void downloadThumbnail(String url, int res, String trackId) throws IOException {
IOUtils.writeToFile(MusicCacheStorageUtils.getTrackThumb(trackId, res), IOUtils.readFully(new URL(url).openStream()));
}
}
@@ -4,11 +4,10 @@ import android.util.Log;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.music.Callback;
import ru.vtosters.lite.music.M3UDownloader;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.FileCacheImplementation;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.music.interfaces.Callback;
import ru.vtosters.lite.music.interfaces.ITrackDownloader;
import ru.vtosters.lite.utils.music.MusicCacheStorageUtils;
import java.io.File;
@@ -24,7 +23,7 @@ public class TrackDownloader {
}
public static void cacheTrack(MusicTrack track, Callback callback) {
if (CacheDatabaseDelegate.isCached(track.y1())) {
if (MusicCacheImpl.isCachedTrack(LibVKXClient.asId(track))) {
callback.onSuccess();
return;
} else if (track.D.isEmpty()) {
@@ -33,7 +32,7 @@ public class TrackDownloader {
return;
}
var path = FileCacheImplementation.getTrackFolder(LibVKXClient.asId(track)).getAbsolutePath();
var path = MusicCacheStorageUtils.getTrackDirById(LibVKXClient.asId(track)).getAbsolutePath();
download(track, path, callback, M3UDownloader.getInstance(), true);
}
@@ -1,4 +1,4 @@
package ru.vtosters.lite.music;
package ru.vtosters.lite.music.interfaces;
public interface Callback {
void onProgress(int progress);
@@ -1,7 +1,6 @@
package ru.vtosters.lite.music.interfaces;
import com.vk.dto.music.MusicTrack;
import ru.vtosters.lite.music.Callback;
import java.io.File;
@@ -23,7 +23,7 @@ public class ThemesHacks {
}
public static int getHackedColor(@NonNull Context context, @ColorRes int color) {
if (ThemesUtils.isMonetTheme()) {
if (ThemesUtils.isMonetTheme() || ThemesManager.canApplyCustomAccent()) {
if (Preferences.getBoolValue("logColors", false)) {
try {
Log.d("ThemesCore", "Requesting color by color " + context.getResources().getResourceName(color));
@@ -3,7 +3,6 @@ package ru.vtosters.lite.themes;
import android.app.Application;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import com.google.devrel.gmscore.tools.apk.arsc.BinaryResourceFile;
import com.vtosters.lite.R;
import ru.vtosters.hooks.other.Preferences;
@@ -2,11 +2,9 @@ package ru.vtosters.lite.themes.loaders;
import android.annotation.SuppressLint;
import android.content.res.AssetManager;
import android.util.Log;
import ru.vtosters.lite.utils.ReflectionUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi")
public class AssetManagerHelper {
@@ -14,7 +14,6 @@ import ru.vtosters.lite.utils.ReflectionUtils;
import java.io.File;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -11,6 +11,7 @@ import android.widget.TextView;
import com.vk.core.drawable.RecoloredDrawable;
import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.themes.ColorReferences;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.utils.AndroidUtils;
public class RecolorUtils {
@@ -43,7 +44,7 @@ public class RecolorUtils {
}
public static int recolorHexColor(int i) {
if (!ThemesUtils.isMonetTheme()) return i;
if (!ThemesUtils.isMonetTheme() || !ThemesManager.canApplyCustomAccent()) return i;
var accented = ColorReferences.isAccentedColor(i);
var mutedaccented = ColorReferences.isMutedAccentedColor(i);
return (accented || mutedaccented) ? (accented ? ThemesUtils.getAccentColor() : ThemesUtils.getMutedAccentColor()) : i;
@@ -2,6 +2,7 @@ package ru.vtosters.lite.ui;
import android.graphics.drawable.Drawable;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
@@ -334,6 +335,52 @@ public class PreferenceFragmentUtils {
});
}
public static Preference addEditTextValPreference(
PreferenceScreen screen,
String key,
CharSequence title,
CharSequence defval,
onPreferenceTextValueChangeListener editTextPrefChangeListener
) {
return addPreference(screen, key, title, null, null, preference -> {
LinearLayout linearLayout = new LinearLayout(screen.getContext());
final EditText editText = new EditText(screen.getContext());
editText.setText(PreferenceManager.getDefaultSharedPreferences(screen.getContext()).getString(key, ""));
editText.setHint(title);
editText.setTextColor(ThemesUtils.getTextAttr());
editText.setHintTextColor(ThemesUtils.getSTextAttr());
editText.setText(defval);
editText.setBackgroundTintList(ThemesUtils.getAccenedColorStateList());
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.setTransformationMethod(null);
linearLayout.addView(editText);
editText.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
ViewGroup.MarginLayoutParams margin = ((ViewGroup.MarginLayoutParams) editText.getLayoutParams());
margin.setMargins(AndroidUtils.dp2px(20f), 0, AndroidUtils.dp2px(20f), 0);
editText.setLayoutParams(margin);
new VkAlertDialog.Builder(screen.getContext())
.setTitle(title)
.setView(linearLayout)
.setPositiveButton("OK", (dialog, which) -> {
boolean change = editTextPrefChangeListener.onChanged(preference, editText.getText().toString());
if (!change)
return;
PreferenceManager.getDefaultSharedPreferences(screen.getContext())
.edit()
.putString(key, editText.getText().toString())
.apply();
})
.show();
return false;
});
}
public interface onPreferenceTextValueChangeListener {
boolean onChanged(Preference preference, String newValue);
}
@@ -1,6 +1,7 @@
package ru.vtosters.lite.ui.adapters;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,6 +29,7 @@ import ru.vtosters.hooks.SwitchHook;
import ru.vtosters.lite.tgs.TGPref;
import ru.vtosters.lite.tgs.TGRoot;
import ru.vtosters.lite.ui.components.IItemMovingListener;
import ru.vtosters.lite.ui.fragments.VTSettings;
import java.io.File;
import java.util.ArrayList;
@@ -138,7 +140,11 @@ public class StickerPackAdapter extends RecyclerView.Adapter<StickerPackAdapter.
});
mName.setText(pack.title);
mSwitch.setChecked(pack.enabled);
mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> sService.setPackEnabled(pack, isChecked, true));
mSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
sService.setPackEnabled(pack, isChecked, false);
buttonView.getContext().sendBroadcast(new Intent(VTSettings.ACTION_INVALIDATE_TGS_COUNT));
});
mStickersCount.setText(pack.stickersCount + " " + mStickersCount.getContext().getString(R.string.stickerscount));
SwitchHook.setSwitchCompatColors(mSwitch, mSwitch.getContext());
@@ -1,39 +0,0 @@
package ru.vtosters.lite.ui.dialogs;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vtosters.lite.R;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.deviceinfo.OEMDetector;
public class DisableBattery {
public static void alert(Activity activity) {
if (!(OEMDetector.isOEM() && Build.VERSION.SDK_INT >= 23 && Preferences.getBoolValue("showDoze", true)))
return;
String packName = activity.getPackageName();
PowerManager manager = (PowerManager) activity.getSystemService(Context.POWER_SERVICE);
if (manager.isIgnoringBatteryOptimizations(packName)) return;
new VkAlertDialog.Builder(activity)
.setTitle(R.string.batteryissuetitle)
.setMessage(R.string.batteryissuesumm)
.setCancelable(false)
.setPositiveButton(R.string.batteryissuebtn1, (dialogInterface, i) -> {
@SuppressLint("BatteryLife")
var intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("package:" + packName));
activity.startActivity(intent);
})
.setNeutralButton(R.string.batteryissuebtn2,
(dialogInterface, i) -> Preferences.getPreferences().edit().putBoolean("showDoze", false).apply()
)
.show();
}
}
@@ -5,6 +5,7 @@ import android.app.Activity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.vtosters.lite.R;
import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.downloaders.OTADownloader;
@@ -17,22 +18,25 @@ public class OTADialog extends ModalBottomSheetWrapper<OTADialog> implements OTA
private final OTAUtils mHelper;
public OTADialog(Activity activity) {
public OTADialog(Activity activity, boolean isManualCheck) {
super(activity);
mActivity = activity;
mHelper = new OTAUtils(this);
mHelper.loadData();
mHelper.loadData(isManualCheck);
}
public static void checkUpdates(Activity activity) {
new OTADialog(activity);
new OTADialog(activity, false);
}
public static void checkUpdatesManual(Activity activity) {
new OTADialog(activity, true);
}
@Override
public void onUpdateApplied() {
public void onUpdateApplied(boolean isManualCheck) {
mActivity.runOnUiThread(() -> {
// Toast.makeText(mActivity, "Обновления найдены", Toast.LENGTH_SHORT).show();
setTitle(mActivity.getString(R.string.newversion) + " " + mHelper.getNewVersionName())
.setView(makeUpdateInfoView(mHelper.getUpdateDescription()))
.setPositiveButton(mActivity.getString(R.string.updateanddownload), () -> {
@@ -57,9 +61,18 @@ public class OTADialog extends ModalBottomSheetWrapper<OTADialog> implements OTA
}
@Override
public void onUpdateCanceled() {
// mActivity.runOnUiThread(() -> {
// // Toast.makeText(mActivity, "Обновлений не найдено", Toast.LENGTH_SHORT).show();
// });
public void onUpdateLatest(boolean isManualCheck) {
if (isManualCheck) {
mActivity.runOnUiThread(() -> {
Toast.makeText(mActivity, "У вас установлена последняя версия приложения", Toast.LENGTH_SHORT).show();
});
}
}
@Override
public void onUpdateError() {
mActivity.runOnUiThread(() -> {
Toast.makeText(mActivity, "Ошибка проверки обновления", Toast.LENGTH_SHORT).show();
});
}
}
@@ -93,6 +93,7 @@ public class DataSettingsFragment extends TrackedMaterialPreferenceToolbarFragme
FileUtils.l();
AutoPlayCacheHolder.d.a();
MediaStorage.a();
IOUtils.deleteRecursive(AndroidUtils.getGlobalContext().getCacheDir());
updateCacheSize();
})
.setNegativeButton(requireContext().getString(com.vtosters.lite.R.string.cancel), (dialogInterface, i) -> dialogInterface.dismiss())
@@ -125,6 +126,7 @@ public class DataSettingsFragment extends TrackedMaterialPreferenceToolbarFragme
ImEngineExt.a(ImEngine1.a());
AutoPlayCacheHolder.d.a();
MediaStorage.a();
IOUtils.deleteRecursive(AndroidUtils.getGlobalContext().getCacheDir());
}
case 1 -> {
SharedPreferences prefs2 = getContext().getSharedPreferences("stickers_storage", Context.MODE_PRIVATE);
@@ -1,6 +1,9 @@
package ru.vtosters.lite.ui.fragments;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vtosters.lite.R;
public class InstallGMSFragment extends TrackedMaterialPreferenceToolbarFragment {
@@ -8,6 +11,22 @@ public class InstallGMSFragment extends TrackedMaterialPreferenceToolbarFragment
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
addPreferencesFromResource(R.xml.preferences_gms);
findPreference("microg").setOnPreferenceClickListener((preference) -> {
String[] items = {"Уменьшенный MicroG (Vanced MicroG)", "Полноценный MicroG (Замена GMS)"};
new VkAlertDialog.Builder(requireActivity()).setItems(items, (dialog, which) -> {
switch (which) {
case 0 ->
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://github.com/TeamVanced/VancedMicroG/releases/latest/download/microg.apk")));
case 1 ->
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://github.com/microg/GmsCore/releases")));
}
dismiss();
}).show();
return true;
});
}
@Override
@@ -24,7 +24,7 @@ import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.downloaders.AudioDownloader;
import ru.vtosters.lite.downloaders.VideoDownloader;
import ru.vtosters.lite.music.LastFMScrobbler;
import ru.vtosters.lite.music.cache.CacheDatabaseDelegate;
import ru.vtosters.lite.music.cache.MusicCacheImpl;
import ru.vtosters.lite.proxy.ProxyUtils;
import ru.vtosters.lite.ui.adapters.ImagineArrayAdapter;
import ru.vtosters.lite.utils.AccountManagerUtils;
@@ -108,9 +108,9 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
findPreference("lastfm_enabled").setEnabled(false);
}
findPreference("cached_tracks").setSummary(String.format(requireContext().getString(R.string.cached_tracks_counter), CacheDatabaseDelegate.getTrackCount()));
findPreference("cached_tracks").setSummary(String.format(requireContext().getString(R.string.cached_tracks_counter), MusicCacheImpl.getTracksCount()));
findPreference("cached_tracks").setOnPreferenceClickListener(preference -> {
if (CacheDatabaseDelegate.getTrackCount() == 0) {
if (MusicCacheImpl.isEmpty()) {
AndroidUtils.sendToast(requireContext().getString(R.string.no_cache_error));
} else {
delcache(requireContext());
@@ -123,6 +123,8 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
return true;
});
findPreference("invertCachedTracks").setVisible(!MusicCacheImpl.isEmpty());
if (!LibVKXClient.isVkxInstalled()) {
findPreference("vkx_sett").setVisible(false);
}
@@ -215,7 +217,7 @@ public class MediaFragment extends TrackedMaterialPreferenceToolbarFragment {
.setTitle(R.string.warning)
.setMessage(R.string.cached_tracks_remove_confirm)
.setPositiveButton(R.string.yes,
(dialog, which) -> executor.submit(CacheDatabaseDelegate::clear))
(dialog, which) -> executor.submit(MusicCacheImpl::clear))
.setNeutralButton(R.string.no,
(dialog, which) -> dialog.cancel())
.show();
@@ -4,6 +4,11 @@ import android.app.Activity;
import android.content.*;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.view.ContextThemeWrapper;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.activities.LockCreationActivity;
import com.vk.auth.api.VKAccount;
import com.vk.core.util.ToastUtils;
import com.vk.pushes.PushSubscriber;
@@ -65,7 +70,7 @@ public class OtherFragment extends TrackedMaterialPreferenceToolbarFragment {
findPreference("microgsettings").setOnPreferenceClickListener(preference -> {
try {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(new ComponentName("com.mgoogle.android.gms", "org.microg.gms.ui.SettingsActivity"));
intent.setComponent(new ComponentName((GmsHook.isFakeGms2Installed() ? "app.revanced.android.gms" : "com.mgoogle.android.gms"), "org.microg.gms.ui.SettingsActivity"));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
@@ -73,6 +78,22 @@ public class OtherFragment extends TrackedMaterialPreferenceToolbarFragment {
return true;
});
findPreference("pin_setup").setOnPreferenceClickListener(preference -> {
var intent = new Intent(this.getActivity(), LockCreationActivity.class);
startActivityForResult(intent, AppLock.REQUEST_CODE_LOCK_CREATION);
return true;
});
findPreference("pin_reset").setOnPreferenceClickListener(preference -> {
if (AppLock.isEnrolled(requireContext())) {
AppLock.getInstance(requireContext()).invalidateEnrollments();
AndroidUtils.sendToast(AndroidUtils.getString("pin_reset"));
} else {
AndroidUtils.sendToast(AndroidUtils.getString("pin_not_set"));
}
return true;
});
findPreference("tokencopy").setOnPreferenceClickListener(preference -> {
copyText(AccountManagerUtils.getUserToken());
@@ -103,13 +124,6 @@ public class OtherFragment extends TrackedMaterialPreferenceToolbarFragment {
return true;
});
findPreference("datasettings").setOnPreferenceClickListener(preference -> {
NavigatorUtils.switchFragment(requireContext(), DataSettingsFragment.class);
return true;
});
findPreference("dialogrecomm").setVisible(!Preferences.hasVerification());
findPreference("updateverifdata").setOnPreferenceClickListener(preference -> {
UsersList.getUsersList();
VTVerifications.load(requireContext());
@@ -6,6 +6,7 @@ import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import com.vtosters.lite.R;
import ru.vtosters.hooks.GmsHook;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.lite.deviceinfo.OEMDetector;
import ru.vtosters.lite.ui.PreferenceFragmentUtils;
@@ -46,6 +47,11 @@ public class SystemInfo extends TrackedMaterialPreferenceToolbarFragment {
boolean isEMUI = OEMDetector.isEMUI();
boolean hasMiuiIncrCode = OEMDetector.hasMiuiIncrCode();
boolean hasGMS = GmsHook.isGmsInstalled();
boolean hasVancedMicroG = GmsHook.isFakeGmsInstalled();
boolean hasReVancedMicroG = GmsHook.isFakeGms2Installed();
String GMSPackageName = hasGMS ? "com.google.android.gms" : hasVancedMicroG ? "com.mgoogle.android.gms" : hasReVancedMicroG ? "app.revanced.android.gms" : "no gms or microg";
boolean isValidSignature = Preferences.isValidSignature();
boolean isTablet = AndroidUtils.isTablet();
boolean isDebuggable = AndroidUtils.isDebuggable();
@@ -76,6 +82,12 @@ public class SystemInfo extends TrackedMaterialPreferenceToolbarFragment {
return false;
});
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "Google Market Services package", GMSPackageName, 0, preference -> {
((ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("MBH-ST", GMSPackageName));
AndroidUtils.sendToast(requireContext().getString(R.string.copied_to_clipboard));
return false;
});
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "Valid Signature", "Value: " + isValidSignature, 0, null);
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "isTablet", "Value: " + isTablet, 0, null);
@@ -90,6 +102,12 @@ public class SystemInfo extends TrackedMaterialPreferenceToolbarFragment {
return false;
});
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "isInstalledGoogleGMS", "Value: " + hasGMS, 0, null);
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "isInstalledVancedMicroG", "Value: " + hasVancedMicroG, 0, null);
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "isInstalledReVancedMicroG", "Value: " + hasReVancedMicroG, 0, null);
PreferenceFragmentUtils.addPreference(getPreferenceScreen(), "", "Product Name", productName, 0, preference -> {
((ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("MBH-ST", productName));
AndroidUtils.sendToast(requireContext().getString(R.string.copied_to_clipboard));
@@ -13,7 +13,6 @@ import com.vtosters.lite.R;
import ru.vtosters.hooks.other.Preferences;
import ru.vtosters.hooks.other.ThemesUtils;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.themes.ThemesCore;
import ru.vtosters.lite.themes.ThemesManager;
import ru.vtosters.lite.themes.palettes.PalettesManager;
import ru.vtosters.lite.ui.components.DockBarEditorManager;
@@ -219,7 +218,7 @@ public class ThemesFragment extends TrackedMaterialPreferenceToolbarFragment {
requireActivity().runOnUiThread(() -> {
dialog.dismiss();
new VkAlertDialog.Builder(requireContext())
.setTitle(AndroidUtils.getString("error"))
.setTitle(AndroidUtils.getString(R.string.error))
.setMessage(AndroidUtils.getString("error_applying_accent") + ":\n" + e)
.setPositiveButton("OK", null)
.show();
@@ -1,16 +1,24 @@
package ru.vtosters.lite.ui.fragments;
import android.annotation.SuppressLint;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import com.aefyr.tsg.g2.TelegramStickersPack;
import com.aefyr.tsg.g2.TelegramStickersService;
import com.vk.about.AboutAppFragment;
import com.vk.balance.BalanceFragment;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vk.identity.fragments.IdentityListFragment;
import com.vk.medianative.MediaImageEncoder;
import com.vk.notifications.settings.NotificationsSettingsFragment;
@@ -35,6 +43,7 @@ import ru.vtosters.lite.BuildConfig;
import ru.vtosters.lite.concurrent.VTExecutors;
import ru.vtosters.lite.deviceinfo.OEMDetector;
import ru.vtosters.lite.ssfs.Utils;
import ru.vtosters.lite.themes.utils.RecolorUtils;
import ru.vtosters.lite.ui.PreferenceFragmentUtils;
import ru.vtosters.lite.ui.dialogs.OTADialog;
import ru.vtosters.lite.ui.fragments.tgstickers.StickersFragment;
@@ -42,7 +51,17 @@ import ru.vtosters.lite.utils.*;
import java.util.Locale;
public class VTSettings extends TrackedMaterialPreferenceToolbarFragment implements TelegramStickersService.StickersEventsListener {
public class VTSettings extends TrackedMaterialPreferenceToolbarFragment {
public static String ACTION_INVALIDATE_TGS_COUNT = "com.vtosters.lite.intent.action.INVALIDATE_TGS_COUNT";
BroadcastReceiver mTgsReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ACTION_INVALIDATE_TGS_COUNT.equals(intent.getAction())) {
findPreference("tgs_stickers").setSummary(getTGSsumm());
}
}
};
public static String getValAsString(@StringRes int strRes, Boolean value) {
if (value) {
@@ -76,46 +95,21 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
ThemesUtils.setTheme(isDarkTheme ? ThemesUtils.getDarkTheme() : ThemesUtils.getLightTheme(), requireActivity(), true);
}
@Override
public void onPackAdded(TelegramStickersPack pack, int atIndex) {
}
@Override
public void onPackRemoved(TelegramStickersPack pack, int atIndex) {
}
@Override
public void onPackChanged(TelegramStickersPack pack, int atIndex) {
}
@Override
public void onPackDownloadError(TelegramStickersPack pack, Exception error) {
}
@Override
public void onActivePacksListChanged() {
findPreference("tgs_stickers").setSummary(getTGSsumm());
}
@Override
public void onInactivePacksListChanged() {
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
TelegramStickersService.getInstance(requireContext()).addStickersEventsListener(this);
requireContext().registerReceiver(mTgsReceiver, new IntentFilter(ACTION_INVALIDATE_TGS_COUNT));
this.addPreferencesFromResource(R.xml.empty);
AndroidUtils.checkLinksVerified(this.requireActivity());
PowerManager manager = (PowerManager) requireActivity().getSystemService(Context.POWER_SERVICE);
boolean isLinksUnverified = AndroidUtils.isLinksUnverified(requireActivity());
boolean isDozingAvailable = Build.VERSION.SDK_INT >= 23 && !manager.isIgnoringBatteryOptimizations(AndroidUtils.getPackageName());
boolean areNotificationsDisabled = !AndroidUtils.areNotificationsEnabled();
boolean isGMSNotInstalled = !GmsHook.isAnyServicesInstalled();
if (AccountManagerUtils.isLogin()) {
Preference accountSwitcher = PreferenceFragmentUtils.addPreference(
@@ -128,6 +122,7 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
try {
VKAuth.a("logout", false);
} catch (Exception ignored) {
// ignored
}
Intent intent = new Intent(requireContext(), MainActivity.class)
@@ -175,8 +170,8 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
AndroidUtils.sendToast(AndroidUtils.getString("systemtheme_enabled"));
return false;
}
final MaterialSwitchPreference switchPreference = (MaterialSwitchPreference) preference;
final boolean isDarkTheme = !switchPreference.isChecked();
MaterialSwitchPreference switchPreference = (MaterialSwitchPreference) preference;
boolean isDarkTheme = !switchPreference.isChecked();
switchTheme(isDarkTheme);
return true;
}
@@ -191,27 +186,89 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
R.drawable.ic_palette_outline_28,
Preferences.systemtheme(),
(preference, o) -> {
Preferences.getPreferences().edit().putBoolean("system_theme", (boolean) o).commit();
Preferences.getPreferences().edit().putBoolean("system_theme", (boolean) o).apply();
SystemThemeChangerHook.onThemeChanged(requireActivity().getResources().getConfiguration());
return true;
}
);
}
if (!GmsHook.isAnyServicesInstalled()) {
PreferenceFragmentUtils.addPreferenceCategory(getPreferenceScreen(), R.string.gmsname);
if ((isLinksUnverified || isDozingAvailable || areNotificationsDisabled || isGMSNotInstalled) && !Preferences.getBoolValue("dialogrecomm", false)) {
PreferenceFragmentUtils.addPreferenceCategory(getPreferenceScreen(), "Рекомендации");
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
requireContext().getString(R.string.installgms),
"",
R.drawable.ic_about_outline_28,
preference -> {
NavigatorUtils.switchFragment(requireContext(), InstallGMSFragment.class);
return false;
}
);
if (isGMSNotInstalled) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
requireContext().getString(R.string.installgms),
"Отсутствие этих сервисов приводит к поломке фоновых уведомлений и проблемам работы компонентов приложения",
RecolorUtils.recolorDrawable(R.drawable.ic_logo_google_28, ThemesUtils.getColor(R.color.red)),
preference -> {
NavigatorUtils.switchFragment(requireContext(), InstallGMSFragment.class);
return false;
}
);
}
if (isLinksUnverified) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
"Не выбраны ссылки для открытия приложением",
"Это помешает открытию внешних ссылок для их открытия с помощью этого приложения\n\nВ некоторых случаях необходимо отключить открытие ссылок официальным приложениям ВКонтакте",
RecolorUtils.recolorDrawable(R.drawable.ic_linked_outline_28, ThemesUtils.getColor(R.color.red)),
preference -> {
try {
Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, Uri.parse("package:" + AndroidUtils.getPackageName()));
requireActivity().startActivity(intent);
} catch (Throwable t1) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + AndroidUtils.getPackageName()));
requireActivity().startActivity(intent);
} catch (Throwable ignored) {
// ignored
}
}
return false;
}
);
}
if (isDozingAvailable) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
"Отключите экономию батареи",
"Экономия батареи мешает получению фоновых сообщений, уведомлений и работе музыки" + (OEMDetector.isMIUI() ? "\n\nДля устройств Xiaomi необходимо вручную включить в настройках системы пункт Автозапуск данному приложению" : ""),
RecolorUtils.recolorDrawable(R.drawable.ic_filter_outline_28, ThemesUtils.getColor(R.color.red)),
preference -> {
@SuppressLint("BatteryLife")
var intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.setData(Uri.parse("package:" + AndroidUtils.getPackageName()));
requireActivity().startActivity(intent);
return false;
}
);
}
if (areNotificationsDisabled) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
"Включите уведомления",
"Без включенных уведомлений не получится получать сообщения и многое другое",
RecolorUtils.recolorDrawable(R.drawable.ic_notifications_outline_28, ThemesUtils.getColor(R.color.red)),
preference -> {
@SuppressLint("BatteryLife")
var intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Settings.EXTRA_APP_PACKAGE, AndroidUtils.getPackageName());
requireActivity().startActivity(intent);
return false;
}
);
}
}
if (Preferences.devmenu()) {
@@ -456,7 +513,7 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
getPreferenceScreen(),
"",
requireContext().getString(R.string.vtlinterface),
getValAsString(R.string.showstories, Preferences.stories()),
getValAsString(R.string.show_superapps_title, Preferences.superapp()),
R.drawable.ic_interface_outline_28,
preference -> {
NavigatorUtils.switchFragment(requireContext(), InterfaceFragment.class);
@@ -464,6 +521,18 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
}
);
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"currcache",
AndroidUtils.getString(R.string.datasettings_title),
getString(com.vtosters.lite.R.string.cache_size_summ) + ": " + CacheUtils.humanReadableByteCountBin(IOUtils.getDirSize(AndroidUtils.getGlobalContext().getCacheDir())),
R.drawable.ic_document_outline_28,
preference -> {
NavigatorUtils.switchFragment(requireContext(), DataSettingsFragment.class);
return false;
}
);
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
@@ -496,14 +565,7 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
AndroidUtils.getString("currentLanguage") + " " + AndroidUtils.upString(Locale.getDefault().getDisplayLanguage()),
R.drawable.ic_globe_outline_28,
preference -> {
try {
Intent intent = new Intent("android.settings.APP_LOCALE_SETTINGS", Uri.parse("package:" + AndroidUtils.getPackageName()));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
AndroidUtils.sendToast("Ваша система не поддерживает смену языка приложения");
}
startActivity(new Intent("android.settings.APP_LOCALE_SETTINGS", Uri.parse("package:" + AndroidUtils.getPackageName())));
return false;
}
);
@@ -535,18 +597,71 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
}
);
if (AndroidUtils.isAdbOrDeveloperOptionsEnabled(requireContext().getContentResolver())) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
requireContext().getString(R.string.opencommit),
"",
R.drawable.ic_link_outline_28,
preference -> {
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AboutHook.getCommitLink())));
return false;
}
);
}
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
requireContext().getString(R.string.opencommit),
"",
R.drawable.ic_link_outline_28,
"Наши чаты",
"Активные чаты по обсуждению модификации и многому другому",
R.drawable.ic_message_outline_28,
preference -> {
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(AboutHook.getCommitLink())));
var args = new Bundle();
var dialog = new AboutInformation();
args.putString("vk_link", "https://vk.me/join/jKE1bIZczHTF2mLkuoiQcd0qIBYgHuGFPRA=");
args.putString("tg_link", "https://t.me/vtosterschat");
dialog.setArguments(args);
dialog.show(requireFragmentManager(), "");
return false;
}
);
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
"Наши сообщества",
"Самая свежая информация об обновлениях и не только!",
R.drawable.users_3_outline_28,
preference -> {
var args = new Bundle();
var dialog = new AboutInformation();
args.putString("vk_link", "https://vk.com/vtosters_official");
args.putString("tg_link", "https://t.me/vtosters");
dialog.setArguments(args);
dialog.show(requireFragmentManager(), "");
return false;
}
);
if (!Preferences.hasVerification() && !Preferences.getBoolValue("dialogrecomm", false)) {
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
"",
"Помочь проекту",
"За донат можно получить приятные бонусы на аккаунт",
R.drawable.ic_money_circle_outline_28,
preference -> {
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://vtosters.app/donate")));
return false;
}
);
}
PreferenceFragmentUtils.addPreference(
getPreferenceScreen(),
@@ -582,16 +697,27 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
"",
R.drawable.ic_download_outline_28,
preference -> {
OTADialog.checkUpdates(getActivity());
OTADialog.checkUpdatesManual(getActivity());
return false;
}
);
}
}
@Override
public void onDestroy() {
requireContext().unregisterReceiver(mTgsReceiver);
super.onDestroy();
}
@Override
public void onResume() {
super.onResume();
updateCacheSize();
}
private void updateCacheSize() {
findPreference("currcache").setSummary(getString(com.vtosters.lite.R.string.cache_size_summ) + ": " + CacheUtils.humanReadableByteCountBin(IOUtils.getDirSize(AndroidUtils.getGlobalContext().getCacheDir())));
}
@Override
@@ -599,9 +725,24 @@ public class VTSettings extends TrackedMaterialPreferenceToolbarFragment impleme
return R.string.notification_settings;
}
@Override
public void onDestroyView() {
super.onDestroyView();
TelegramStickersService.getInstance(requireContext()).removeStickersEventsListener(this);
public static class AboutInformation extends DialogFragment {
@Override
public AlertDialog onCreateDialog(Bundle savedInstanceState) {
VkAlertDialog.Builder builder = new VkAlertDialog.Builder(requireActivity());
String[] items = {AndroidUtils.getString(R.string.app_name_alter), "Telegram"};
builder.setItems(items, (dialog, which) -> {
switch (which) {
case 0 ->
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(getArguments().getString("vk_link"))));
case 1 ->
requireContext().startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(getArguments().getString("tg_link"))));
}
dismiss();
});
return builder.create();
}
}
}
@@ -20,7 +20,6 @@ import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.StaggeredGridLayoutManager;
import com.aefyr.tsg.g2.TelegramStickersService;
import com.aefyr.tsg.g2.stickersgrabber.TelegramStickersGrabber;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.vk.core.dialogs.alert.VkAlertDialog;
import com.vtosters.lite.R;
import ru.vtosters.hooks.other.ThemesUtils;
@@ -36,55 +35,52 @@ import java.io.File;
public class StickersFragment extends BaseToolbarFragment {
public final static String ACTION_RELOAD = "com.vtosters.lite.action.RELOAD_TGS_LIST";
private final static int TYPE_DIRECT = 0, TYPE_SOCKS = 2;
public int from;
public int to;
private TelegramStickersGrabber mGrabber;
private TelegramStickersService mService;
private StickerPackAdapter mAdapter;
private boolean movePending = false;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
final static int TYPE_DIRECT = 0;
final static int TYPE_SOCKS = 2;
TelegramStickersGrabber mGrabber;
StickerPackAdapter mAdapter;
boolean movePending = false;
int from, to;
final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@SuppressLint("NotifyDataSetChanged")
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_RELOAD)) {
if (mAdapter != null) {
try {
if (movePending) {
movePending = false;
mAdapter.notifyItemMoved(from, to);
from = -1;
to = -1;
} else {
mAdapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (!intent.getAction().equals(ACTION_RELOAD) || mAdapter == null) return;
try {
if (movePending) {
movePending = false;
mAdapter.notifyItemMoved(from, to);
from = to = -1;
} else mAdapter.notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
}
};
@Override
protected void onCreateMenu(Menu menu) {
MenuItem item = menu.add(0, 0, 0, "");
item.setIcon(ThemesUtils.recolorDrawable(AndroidUtils.getGlobalContext().getDrawable(R.drawable.ic_settings_outline_28)))
menu.add(0, 0, 0, "")
.setIcon(ThemesUtils.recolorDrawable(requireContext().getDrawable(R.drawable.ic_add_outline_28)))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
menu.add(0, 1, 0, "")
.setIcon(R.drawable.ic_settings_outline_28)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
super.onCreateMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem arg0) {
openMenu(null);
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case 0 -> addTgsPack();
case 1 -> openMenu(null);
}
return true;
}
private void openMenu(String toast) {
if (toast != null) {
Toast.makeText(requireContext(), toast, Toast.LENGTH_SHORT).show();
}
if (toast != null) Toast.makeText(requireContext(), toast, Toast.LENGTH_SHORT).show();
NavigatorUtils.switchFragment(requireContext(), StickersPreferencesFragment.class);
}
@@ -94,40 +90,28 @@ public class StickersFragment extends BaseToolbarFragment {
requireContext().registerReceiver(mReceiver, new IntentFilter(ACTION_RELOAD));
mGrabber = new TelegramStickersGrabber(TGPref.getTGBotKey());
mService = TelegramStickersService.getInstance(requireContext());
}
@Override
public View onCreateContent(@NonNull LayoutInflater inflater, @Nullable Bundle bundle) {
setTitle(R.string.vtltgs);
FrameLayout layout = new FrameLayout(requireContext());
final var container = new FrameLayout(requireContext());
final var recycler = new RecyclerView(requireContext());
mAdapter = new StickerPackAdapter();
RecyclerView mRecycler = new RecyclerView(requireContext());
recycler.setAdapter(mAdapter);
StickerTouchHelperCallback callback = new StickerTouchHelperCallback(mAdapter);
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(mRecycler);
final var callback = new StickerTouchHelperCallback(mAdapter);
final var touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recycler);
mRecycler.setAdapter(mAdapter);
mRecycler.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
layout.addView(mRecycler, new ViewGroup.LayoutParams(-1, -1));
recycler.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
container.addView(recycler, new ViewGroup.LayoutParams(-1, -1));
FloatingActionButton mAddStickerPack = new FloatingActionButton(requireContext());
mAddStickerPack.setImageResource(R.drawable.ic_add_24);
mAddStickerPack.setBackgroundTintList(ThemesUtils.getAccenedColorStateList());
mAddStickerPack.setOnClickListener(v2 -> fabClick());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(-2, -2);
params.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
params.setMargins(0, 0, 0, AndroidUtils.dp2px(12f));
layout.addView(mAddStickerPack, params);
return layout;
return container;
}
@Override
public void onViewCreated(View arg0, Bundle arg1) {
super.onViewCreated(arg0, arg1);
@@ -139,63 +123,62 @@ public class StickersFragment extends BaseToolbarFragment {
requireContext().unregisterReceiver(mReceiver);
}
private void fabClick() {
int method = TGPref.getTGConnectMethod();
if (method == -1) {
openMenu(requireContext().getString(R.string.stickersconnection));
return;
}
private void addTgsPack() {
final var method = TGPref.getTGConnectMethod();
if (method != -1) {
initGrabber();
switch (method) {
case TYPE_SOCKS -> {
if (TGPref.getTGProxyIP() == null) openMenu(requireContext().getString(R.string.stickersproxy1));
}
case TYPE_DIRECT -> {
initGrabber();
final Runnable lambda = () ->
{
final var container = new LinearLayout(requireContext());
initGrabber();
if (method == TYPE_SOCKS) {
if (TGPref.getTGProxyIP() == null) {
openMenu(requireContext().getString(R.string.stickersproxy1));
final var input = new EditText(requireContext());
final var inputLP = new ViewGroup.MarginLayoutParams(-1, -2);
inputLP.setMargins(
AndroidUtils.dp2px(24.f),
0,
AndroidUtils.dp2px(24.f),
0);
container.addView(input, inputLP);
new VkAlertDialog.Builder(requireContext())
.setTitle(R.string.stickershelp1)
// .setMessage(R.string.stickershelp2)
.setView(container)
.setPositiveButton(android.R.string.ok, (dialog, which) ->
{
var packId = input.getText().toString();
if (packId.startsWith("https://"))
packId = packId.substring(packId.lastIndexOf('/') + 1);
TelegramStickersService.getInstance(requireContext())
.requestPackDownload(
packId,
new File(requireContext().getFilesDir(), "VT-Stickers/" + packId));
})
.setNeutralButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.show();
};
if (TGPref.getTGBotKey() == null) enterBotKey(() -> checkApiKey(lambda));
else checkApiKey(lambda);
}
}
} else if (method == TYPE_DIRECT) {
final Runnable callback = () -> {
LinearLayout linearLayout = new LinearLayout(requireContext());
final EditText editText = new EditText(requireContext());
editText.setHintTextColor(ThemesUtils.getSTextAttr());
editText.setBackgroundTintList(ThemesUtils.getAccenedColorStateList());
linearLayout.addView(editText);
editText.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
ViewGroup.MarginLayoutParams margin = ((ViewGroup.MarginLayoutParams) editText.getLayoutParams());
margin.setMargins(AndroidUtils.dp2px(24f), 0, AndroidUtils.dp2px(24f), 0);
editText.setLayoutParams(margin);
new VkAlertDialog.Builder(requireContext())
.setTitle(requireContext().getString(R.string.stickershelp1))
.setMessage(requireContext().getString(R.string.stickershelp2))
.setView(linearLayout)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String pack = editText.getText().toString();
if (!pack.startsWith("https://")) {
pack = "https://" + pack;
} else if (!pack.contains("addstickers")) {
AndroidUtils.sendToast(AndroidUtils.getString("invalid_pack_link"));
}
pack = parsePack(pack);
mService.requestPackDownload(pack, new File(requireContext().getFilesDir(), new File("VT-Stickers", pack).getAbsolutePath()));
})
.setNeutralButton(android.R.string.cancel,
(dialog, which) -> dialog.dismiss()
)
.show();
};
if (TGPref.getTGBotKey() == null) enterBotKey(() -> checkApiKey(callback));
else checkApiKey(callback);
}
} else openMenu(requireContext().getString(R.string.stickersconnection));
}
private void initGrabber() {
int method = TGPref.getTGConnectMethod();
if (method == TYPE_DIRECT) {
TelegramStickersGrabber.USE_PROXY = false;
mGrabber.resetProxy();
} else if (method == TYPE_SOCKS) {
if (TGPref.getTGProxyIP() != null) {
switch (TGPref.getTGConnectMethod()) {
case TYPE_DIRECT -> {
TelegramStickersGrabber.USE_PROXY = false;
mGrabber.resetProxy();
}
case TYPE_SOCKS -> {
if (TGPref.getTGProxyIP() == null) break;
TelegramStickersGrabber.USE_PROXY = true;
TelegramStickersGrabber.PROXY_IP = TGPref.getTGProxyIP();
TelegramStickersGrabber.PROXY_PORT = TGPref.getTGProxyPort();
@@ -210,42 +193,41 @@ public class StickersFragment extends BaseToolbarFragment {
TelegramStickersGrabber.updateURLs();
}
private void enterBotKey(Runnable r) {
LinearLayout linearLayout = new LinearLayout(requireContext());
private void enterBotKey(Runnable callback) {
final var container = new LinearLayout(requireContext());
final EditText editText = new EditText(requireContext());
editText.setHintTextColor(ThemesUtils.getSTextAttr());
editText.setBackgroundTintList(ThemesUtils.getAccenedColorStateList());
linearLayout.addView(editText);
editText.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
ViewGroup.MarginLayoutParams margin = ((ViewGroup.MarginLayoutParams) editText.getLayoutParams());
margin.setMargins(AndroidUtils.dp2px(24f), 0, AndroidUtils.dp2px(24f), 0);
editText.setLayoutParams(margin);
final var input = new EditText(requireContext());
final var inputLP = new ViewGroup.MarginLayoutParams(-1, -2);
inputLP.setMargins(
AndroidUtils.dp2px(24.f),
0,
AndroidUtils.dp2px(24.f),
0);
container.addView(input, inputLP);
new VkAlertDialog.Builder(requireContext())
.setTitle(requireContext().getString(R.string.stickersapi5))
.setMessage(requireContext().getString(R.string.stickersapi6))
.setView(linearLayout)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
TGPref.setTGBotKey(editText.getText().toString());
if (r != null) r.run();
.setView(container)
.setPositiveButton(android.R.string.ok, (dialog, which) ->
{
TGPref.setTGBotKey(input.getText().toString());
if (callback != null) callback.run();
})
.setNegativeButton(android.R.string.cancel,
(dialog, which) -> dialog.dismiss()
)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setNeutralButton(requireContext().getString(R.string.stickersapi7),
(dialog, which) -> new VkAlertDialog.Builder(requireContext())
.setTitle(requireContext().getString(R.string.stickersapi8))
.setMessage(requireContext().getString(R.string.stickersapi9) +
requireContext().getString(R.string.stickersapi10))
.setNeutralButton(AndroidUtils.getString("open_bot"), (dl, i) -> {
var intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/botfather"));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
requireActivity().startActivity(intent);
})
.setPositiveButton(android.R.string.ok, null)
.show()
(dialog, which) ->
new VkAlertDialog.Builder(requireContext())
.setTitle(requireContext().getString(R.string.stickersapi8))
.setMessage(requireContext().getString(R.string.stickersapi9) +
requireContext().getString(R.string.stickersapi10))
.setNeutralButton(AndroidUtils.getString("open_bot"), (dl, i) -> {
final var intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://t.me/botfather"))
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
requireActivity().startActivity(intent);
})
.setPositiveButton(android.R.string.ok, null)
.show()
)
.show();
}
@@ -253,45 +235,35 @@ public class StickersFragment extends BaseToolbarFragment {
private void checkApiKey(Runnable callback) {
mGrabber.setBotApiKey(TGPref.getTGBotKey());
var context = requireContext();
final ProgressDialog progressDialog = new ProgressDialog(context);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setMessage(requireContext().getString(R.string.stickersapi1));
progressDialog.show();
final var dialog = new ProgressDialog(requireContext());
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dialog.setMessage(requireContext().getString(R.string.stickersapi1));
dialog.show();
mGrabber.checkKey(new TelegramStickersGrabber.KeyCheckListener() {
@Override
public void onKeyChecked(boolean ok) {
progressDialog.dismiss();
dialog.dismiss();
if (!ok) {
Toast.makeText(context, requireContext().getString(R.string.stickersapi2), Toast.LENGTH_SHORT).show();
Toast.makeText(requireContext(), requireContext().getString(R.string.stickersapi2), Toast.LENGTH_SHORT).show();
TGPref.setTGBotKey(null);
return;
}
mService.setBotKey(TGPref.getTGBotKey());
TelegramStickersService.getInstance(requireContext())
.setBotKey(TGPref.getTGBotKey());
if (callback != null) callback.run();
}
@Override
public void onNetError() {
progressDialog.dismiss();
new VkAlertDialog.Builder(context)
dialog.dismiss();
new VkAlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.stickersapi3))
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(requireContext().getString(R.string.stickersapi4), (arg0, arg1) -> checkApiKey(callback))
.setPositiveButton(requireContext().getString(R.string.stickersapi4), (di, which) -> checkApiKey(callback))
.show();
}
});
}
private String parsePack(String pack) {
if (pack.startsWith("https://t.me/addstickers/")) {
pack = pack.substring(25);
} else if (pack.startsWith("https://telegram.me/addstickers/")) {
pack = pack.substring(32);
}
return pack;
}
}
@@ -1,8 +1,9 @@
package ru.vtosters.lite.utils;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationManager;
@@ -14,11 +15,10 @@ import android.provider.Settings;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.vk.core.dialogs.alert.VkAlertDialog;
import androidx.core.app.NotificationManagerCompat;
import com.vk.core.util.Screen;
import com.vk.core.util.ToastUtils;
import com.vtosters.lite.general.fragments.WebViewFragment;
import ru.vtosters.hooks.other.Preferences;
import java.io.File;
import java.io.FileOutputStream;
@@ -30,8 +30,6 @@ import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Random;
import static ru.vtosters.hooks.other.Preferences.getBoolValue;
public class AndroidUtils {
private static final String ALLOWED_CHARACTERS = "0123456789qwertyuiopasdfghjklzxcvbnm";
@@ -39,6 +37,12 @@ public class AndroidUtils {
return 0 != (AndroidUtils.getGlobalContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE);
}
public static boolean isAdbOrDeveloperOptionsEnabled(ContentResolver contentResolver) {
int adbEnabled = Settings.Global.getInt(contentResolver, Settings.Global.ADB_ENABLED, 0);
int developerOptionsEnabled = Settings.Global.getInt(contentResolver, Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0);
return adbEnabled == 1 || developerOptionsEnabled == 1;
}
public static boolean isTablet() {
return Screen.l(getGlobalContext());
}
@@ -117,8 +121,8 @@ public class AndroidUtils {
new WebViewFragment.g(url).l().m().h().j().a(activity);
}
public static void checkLinksVerified(Activity activity) {
if (Build.VERSION.SDK_INT < 31) return;
public static boolean isLinksUnverified(Activity activity) {
if (Build.VERSION.SDK_INT < 31) return false;
DomainVerificationManager manager = activity.getSystemService(DomainVerificationManager.class);
DomainVerificationUserState userState;
@@ -126,7 +130,7 @@ public class AndroidUtils {
try {
userState = manager.getDomainVerificationUserState(getPackageName());
} catch (PackageManager.NameNotFoundException ignore) {
return;
return false;
}
boolean hasUnverified = false;
@@ -138,32 +142,7 @@ public class AndroidUtils {
hasUnverified = stateValue != null && stateValue != DomainVerificationUserState.DOMAIN_STATE_VERIFIED && stateValue != DomainVerificationUserState.DOMAIN_STATE_SELECTED;
}
if (hasUnverified) {
if (getBoolValue("showUnverifDialog", true)) {
new VkAlertDialog.Builder(activity)
.setTitle(com.vtosters.lite.R.string.warning)
.setMessage(AndroidUtils.getString("app_open_by_default_settings"))
.setCancelable(false)
.setPositiveButton(com.vtosters.lite.R.string.social_graph_skip,
(dialogInterface, i) -> Preferences.getPreferences().edit().putBoolean("showUnverifDialog", false).apply()
)
.setNeutralButton(com.vtosters.lite.R.string.open_settings,
(dialogInterface, i) -> {
try {
Intent intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, Uri.parse("package:" + getPackageName()));
activity.startActivity(intent);
} catch (Throwable t1) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:" + getPackageName()));
activity.startActivity(intent);
} catch (Throwable ignored) {
}
}
}
)
.show();
}
}
return hasUnverified;
}
public static String MD5(String s) {
@@ -185,6 +164,15 @@ public class AndroidUtils {
return null;
}
public static boolean areNotificationsEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) getGlobalContext().getSystemService(Context.NOTIFICATION_SERVICE);
return manager.areNotificationsEnabled();
} else {
return NotificationManagerCompat.from(getGlobalContext()).areNotificationsEnabled();
}
}
private static String getRandomString(int sizeOfRandomString) {
Random random = new Random();
StringBuilder sb = new StringBuilder(sizeOfRandomString);
@@ -44,7 +44,7 @@ public class ExternalLinkParser {
parseVKVideo(videoFile, qualities, urls);
if (urls.size() > 0)
if (!urls.isEmpty())
if (getBoolValue("maxquality", false) || qualities.size() == 1)
startExternalVideo(context, urls.get(0));
else
@@ -1,40 +1,13 @@
package ru.vtosters.lite.utils;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class IOUtils {
public static int BUFFER_SIZE = 8192;
public static byte[] decodeStream(InputStream encIs, byte[] aesKey)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IOException {
CipherInputStream cip = new CipherInputStream(encIs, getCipher(aesKey));
return readFully(cip);
}
public static Cipher getCipher(byte[] aesKey)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Key keySpec = new SecretKeySpec(aesKey, "AES");
byte[] iv = new byte[16];
Arrays.fill(iv, (byte) 0x0);
IvParameterSpec ivps = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivps);
return cipher;
}
public static InputStream openStream(String url)
throws IOException {
URLConnection connection = new URL(url).openConnection();
@@ -7,6 +7,10 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import com.guardanis.applock.AppLock;
import com.guardanis.applock.activities.UnlockActivity;
import com.vk.core.fragments.FragmentImpl;
import com.vk.navigation.Navigator;
import com.vtosters.lite.MainActivity;
import java.util.ArrayList;
@@ -42,6 +46,11 @@ public class LifecycleUtils {
@Override // android.app.Application.ActivityLifecycleCallbacks
public void onActivityCreated(Activity activity, Bundle bundle) {
activities.add(activity);
if (AppLock.isUnlockRequired(activity) && activity.getClass() != MainActivity.class) {
var intent = new Intent(activity, UnlockActivity.class);
activity.startActivity(intent);
}
}
@Override // android.app.Application.ActivityLifecycleCallbacks
@@ -52,7 +61,7 @@ public class LifecycleUtils {
}
public static Activity getCurrentActivity() {
return activities.size() > 0 ? activities.get(activities.size() - 1) : new MainActivity();
return !activities.isEmpty() ? activities.get(activities.size() - 1) : new MainActivity();
}
public static void restartApplication() {
@@ -22,7 +22,7 @@ public class OTAUtils {
this.mListener = listener;
}
public void loadData() {
public void loadData(boolean isManualCheck) {
Request release = new Request.a()
.b(LATEST_RELEASE_URL)
.a();
@@ -31,13 +31,13 @@ public class OTAUtils {
@Override
public void a(Call call, IOException e) {
Log.d("OTAHelper", "Error while getting latest release info: " + e);
mListener.onUpdateCanceled();
mListener.onUpdateError();
}
@Override
public void a(Call call, Response response) throws IOException {
try {
setData(response);
setData(response, isManualCheck);
} catch (JSONException | NullPointerException e) {
e.printStackTrace();
}
@@ -46,7 +46,7 @@ public class OTAUtils {
});
}
void setData(Response response) throws IOException, JSONException {
void setData(Response response, boolean isManualCheck) throws IOException, JSONException {
mReleaseJson = new JSONObject(response.a().g());
String tag = mReleaseJson.getString("tag_name");
@@ -58,7 +58,7 @@ public class OTAUtils {
@Override
public void a(Call call, IOException e) {
Log.d("OTAHelper", "Error while getting latest commit info: " + e);
mListener.onUpdateCanceled();
mListener.onUpdateError();
}
@Override
@@ -67,12 +67,12 @@ public class OTAUtils {
mCommitJson = new JSONObject(response.a().g());
mCommitSHA = mCommitJson.getJSONObject("object").getString("sha");
if (isNewVersion())
mListener.onUpdateApplied();
mListener.onUpdateApplied(isManualCheck);
else
mListener.onUpdateCanceled();
mListener.onUpdateLatest(isManualCheck);
} catch (NullPointerException | JSONException e) {
e.printStackTrace();
mListener.onUpdateCanceled();
mListener.onUpdateError();
}
}
});
@@ -118,9 +118,10 @@ public class OTAUtils {
public interface OTAListener {
void onUpdateApplied();
void onUpdateApplied(boolean isManualCheck);
void onUpdateCanceled();
void onUpdateError();
void onUpdateLatest(boolean isManualCheck);
}
}
@@ -19,8 +19,8 @@ public class SuperAppUtils {
}
public static JSONObject superapp(JSONObject json) throws JSONException {
var superApps = Preferences.getPreferences().getString("superapp_items",
"menu,miniapps,vkpay_slim,greeting,promo,holiday,weather,sport,games,informer,food,event,music,vk_run").split(",");
var superApps = Preferences.getPreferences().getString("superapp_items", "menu,miniapps,vkpay_slim,greeting,promo,holiday,weather,sport,games,informer,food,event,music,vk_run").split(",");
if (superApps.length == 0) return json;
var oldItems = json.optJSONArray("items");
@@ -31,7 +31,24 @@ public class SuperAppUtils {
var type = item.optString("type");
for (String superApp : superApps) {
if (type.equals(superApp))
newItems.put(item);
if (superApp.equals("menu")) { // superapp menu fix
String menuitems = "groups,audios,stickers,podcasts,shopping,lives,videos,games,vk_calls,vk_pay,ads_easy_promote,classifieds,events";
JSONObject menuItem = new JSONObject();
JSONArray menuItems = new JSONArray();
generateMenuItems(menuitems, menuItems);
JSONObject object = new JSONObject();
object.put("count", menuitems.split(",").length);
object.put("items", menuItems);
menuItem.put("type", "menu");
menuItem.put("object", object);
newItems.put(menuItem);
} else {
newItems.put(item);
}
}
}
for (int i = 0; i < superApps.length; i++) {
@@ -46,4 +63,16 @@ public class SuperAppUtils {
return json.putOpt("items", newItems);
}
private static void generateMenuItems(String str, JSONArray jsonArray) throws JSONException {
String[] strings = str.split(",");
for (String string : strings) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", string);
jsonArray.put(jsonObject);
}
}
}
@@ -7,7 +7,6 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import com.vk.usersstore.contentprovider.a.UsersDbHelper;
import com.vtosters.lite.auth.VKAuth;
@@ -76,7 +75,7 @@ public class VKAccountDB {
var dir = new File(getExternalStoragePublicDirectory(DIRECTORY_DOWNLOADS), "/VTLBackups/");
var file = new File(dir, "saved_accounts_" + dateFormat.format(new Date()) + ".vtl");
try (InputStream inputStream = new FileInputStream(db.getPath())){
try (InputStream inputStream = new FileInputStream(db.getPath())) {
dir.mkdirs();
FileOutputStream outputStream = new FileOutputStream(file);
@@ -56,8 +56,7 @@ public class VTVerifications {
.apply();
isLoaded = true;
Log.d("VTVerifications", "load from network");
} catch (Exception e) {
e.printStackTrace();
} catch (Exception ignored) {
if (prefs.contains("ids")) {
parseJson(prefs.getString("ids", "[]"));
Log.d("VTVerifications", "load from memory. Something went wrong with parsing");
@@ -92,7 +91,7 @@ public class VTVerifications {
processIds(json.optJSONArray("404"), sDevelopers);
processIds(json.optJSONArray("1337"), sServiceAccounts);
} catch (JSONException e) {
e.printStackTrace();
// ignored
}
}
@@ -100,8 +99,13 @@ public class VTVerifications {
if (jsonIds == null || jsonIds.length() == 0)
return;
for (int i = 0; i < jsonIds.length(); i++)
member.add(jsonIds.optInt(i));
for (int i = 0; i < jsonIds.length(); i++) {
try {
member.add(jsonIds.optInt(i));
} catch (Exception ignored) {
// ignored
}
}
}
public static boolean isPrometheus(int id) {
@@ -0,0 +1,42 @@
package ru.vtosters.lite.utils.music;
import ru.vtosters.lite.utils.AndroidUtils;
import ru.vtosters.lite.utils.IOUtils;
import java.io.File;
public class MusicCacheStorageUtils {
public static File getCacheStorageDir() {
return AndroidUtils.getGlobalContext().getExternalFilesDir("vt_tracks");
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static File getTrackDirById(final String trackId) {
final var res = new File(getCacheStorageDir(), trackId);
res.mkdirs();
return res;
}
public static File getTrackFile(final String trackId) {
return new File(getTrackDirById(trackId), "track.mp3");
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static File getThumbDirById(final String trackId) {
final var res = new File(getTrackDirById(trackId), "thumbs");
res.mkdirs();
return res;
}
public static File getTrackThumb(final String trackId, final int factor) {
return new File(getThumbDirById(trackId), "thumb_" + factor + ".png");
}
public static void removeTrackDirById(final String trackId) {
IOUtils.deleteRecursive(getTrackDirById(trackId));
}
public static void clear() {
IOUtils.deleteRecursive(getCacheStorageDir());
}
}
@@ -0,0 +1,47 @@
package ru.vtosters.lite.utils.music;
import android.os.RemoteException;
import bruhcollective.itaysonlab.libvkx.ILibVkxService;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClient;
import bruhcollective.itaysonlab.libvkx.client.LibVKXClientImpl;
public class VKXUtils {
public static boolean isVkxCached(int playlist_id, int owner_id) {
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
@Override
public Boolean run(ILibVkxService service) {
try {
return service.isPlaylistCached(playlist_id, owner_id);
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
public static boolean isVkxCached(String trackId) {
return LibVKXClient.getInstance().runOnServiceSync(new LibVKXClientImpl.LibVKXActionGeneric<>() {
@Override
public Boolean run(ILibVkxService service) {
try {
var splits = trackId.split("_");
var id = Integer.parseInt(splits[1]);
var owner = Integer.parseInt(splits[0]);
return service.isTrackCached(id, owner);
} catch (RemoteException e) {
return false;
}
}
@Override
public Boolean defaultValue() {
return false;
}
});
}
}
+18
View File
@@ -16,6 +16,7 @@
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
@@ -138,6 +139,7 @@
<uses-permission android:name="me.everything.badger.permission.BADGE_COUNT_WRITE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="com.mgoogle.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="app.revanced.android.c2dm.permission.RECEIVE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE" />
@@ -196,6 +198,14 @@
android:name="ru.vtosters.lite.ui.activities.VKAdminTokenActivity"
android:launchMode="singleTop">
</activity>
<activity
android:name="com.guardanis.applock.activities.LockCreationActivity"
android:launchMode="singleTop">
</activity>
<activity
android:name="com.guardanis.applock.activities.UnlockActivity"
android:launchMode="singleTop">
</activity>
<activity
android:name="com.vtosters.lite.MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
@@ -2313,6 +2323,14 @@
<action android:name="com.mgoogle.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>
<receiver
android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver3"
android:exported="true"
android:permission="app.revanced.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="app.revanced.android.c2dm.intent.RECEIVE" />
</intent-filter>
</receiver>
<activity
android:name="com.google.android.gms.common.api.GoogleApiActivity"
android:exported="false"
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/applock__create_choose_option_bg_pressed" android:state_selected="true"/>
<item android:drawable="@color/applock__create_choose_option_bg_pressed" android:state_pressed="true"/>
<item android:drawable="@color/applock__create_choose_option_bg" />
</selector>

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