Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
Li | a6dc9fbd93 | |
Li | 83b155437f | |
Li | 6fcf0643c7 | |
Li | 5dfdea82d0 | |
random() | d3838fdb28 | |
Li | 3305933dc9 | |
Li | d904e40b77 | |
Li | a416efaebc | |
Li | 513f30b1da | |
Li | 584e6c913d | |
Li | 6e157330cf | |
Li | 0fce254917 | |
Li | f821147c21 | |
Li | f7555574d6 | |
Li | 6acd9fb269 | |
Li | 780278c9fa | |
Li | 8bc60d1083 | |
Li | 68e38ba358 | |
Li | 9fae0324af | |
Li | 08b6881b63 | |
random() | 3e3dfb1a50 | |
Li | 1b93b198b2 | |
Li | bba0b4e3ea | |
Li | 997c50ec3e | |
Li | d6ab6ee992 | |
Li | 9eb8331d85 | |
Li | 9da9468621 | |
Li | d53984fa48 | |
Li | 076ec14d2b | |
Li | 295cd7458d | |
Li | 2a9098c8c0 | |
Li | 0d774e35af | |
Li | dd58d20549 | |
Li | b363371fe6 | |
Li | 75ee1d7b16 | |
Li | 50180c78e1 | |
Li | d8d7134c1e | |
Li | 017012413f | |
Li | c4cddf38c6 | |
Li | 7420b7b661 | |
Li | e56db07fd7 | |
Li | 0dc733ac8d | |
Li | 5f6a88068a | |
Li | 27c90807e6 | |
Li | edd951acae | |
Li | be9ddbdc04 | |
Li | 648c741ee0 | |
Li | 1bd7d90db8 | |
Li | fb7d7d4acf | |
Li | 1540612636 | |
Li | 0ab7b80ab0 | |
Li | 9c22b0f170 | |
Li | 29e0391fdc | |
Li | d25ff54375 | |
Li | 8cd3a09907 | |
Li | 14fbc30116 |
|
@ -15,4 +15,7 @@
|
|||
.cxx
|
||||
local.properties
|
||||
libsuperuser/build/*
|
||||
app/build/*
|
||||
app/build/*
|
||||
app/release/*
|
||||
|
||||
libNpTicket/*
|
|
@ -1 +1 @@
|
|||
nopsmdrm
|
||||
nopssdrm
|
|
@ -10,6 +10,7 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/libABX" />
|
||||
<option value="$PROJECT_DIR$/libsuperuser" />
|
||||
</set>
|
||||
</option>
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
|
@ -0,0 +1,50 @@
|
|||
# Running on modern android
|
||||
|
||||
So there are a few things on modern android versions, that kind of get in the way of using the PSM app
|
||||
|
||||
- Since android 6, you can no longer just remount /system/ as read-write, (even as root)
|
||||
this makes it hard to install the "PlayStation Certificates".
|
||||
|
||||
- Since android 7, android_id is unique *per-app* instead of *per-device*,
|
||||
this means that when generating the signin cache,
|
||||
you have to find the android_id used *by psm*.
|
||||
|
||||
- Since android 10, it's no longer possible for apps to just write to ``/android/data/com.playstation.psstore/files/psm``
|
||||
which is where the PSM Application *stores* PSM games, however- it is still possible to write there using MTP or ADB.
|
||||
BUT the file permissions get messed up when you do this.
|
||||
|
||||
- Since android 14, android refuses to install any application targeting SDK <23.
|
||||
this includes PSM, and also. NoPsmDrm because it's trying to be compatible with old android also.
|
||||
|
||||
There are some workarounds for all of these.
|
||||
using PSM on newer versions can be a bit annoying.
|
||||
|
||||
|
||||
To get the app working on modern android versions; you need to:
|
||||
|
||||
First, install the PlayStation Certificates *systemlessly* via a Magisk Module.
|
||||
i have made one to do this and it can be found here; https://silica.codes/Li/MagiskCertify (Android 6+)
|
||||
|
||||
After that, you can install the PlayStation Mobile app, *and* the NoPsmDrm app,
|
||||
if your on android >14, this *has* to be done via ADB with USB Debugging
|
||||
because that gives you access to the `--bypass-low-target-sdk-block` argument
|
||||
|
||||
(You will get a Play Protect warning for both of these)
|
||||
|
||||
```adb install --bypass-low-target-sdk-block PSM.apk```
|
||||
|
||||
```adb install --bypass-low-target-sdk-block NoPsmDrm-Android.apk```
|
||||
|
||||
Once installed, you need to tap on both apps and goto permissions and grant *everything*
|
||||
older android versions (prior to 6.0) handled permissions differnetly and expected to have
|
||||
everything granted at install time .. so make sure you do that!
|
||||
|
||||
Workaround for android_id on Android 7+, once done that open NoPsmDrm, and click install.
|
||||
it will then tell you to open the PSM app, do that,
|
||||
wait for the 'failed to connect' message, and then just close the PSM app.
|
||||
return to NoPsmDrm and click install once more and *that time* it should work.
|
||||
|
||||
Now you can just copy PSM games to ``/sdcard/android/data/com.playstation.psstore/files/psm``
|
||||
however as of android 10, you can *only* do this via ADB *or* via the USB connection to your phone
|
||||
|
||||
if after doing this you do not see the games pop up in the PSM app try the "Fix permissions" button in NoPsmDrm.
|
|
@ -4,15 +4,16 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace 'com.psmreborn.nopsmdrm'
|
||||
//noinspection GradleDependency needs to work properly on android 2.x
|
||||
compileSdk 10
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.psmreborn.nopsmdrm"
|
||||
minSdk 10
|
||||
//noinspection ExpiredTargetSdkVersion dont care about google play
|
||||
targetSdk 10
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
//noinspection ExpiredTargetSdkVersion app is not distributed on google play -- this app breaks all its rules anyway
|
||||
targetSdk 23
|
||||
versionCode 193
|
||||
versionName "1.9.3"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -24,12 +25,18 @@ android {
|
|||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
applicationVariants.configureEach { variant ->
|
||||
variant.outputs.configureEach {
|
||||
def versionName = variant.versionName
|
||||
outputFileName = "${applicationName}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':libsuperuser')
|
||||
implementation project(':libABX')
|
||||
implementation 'yuv.pink:npticket:1.6'
|
||||
//noinspection GradleDependency new version has min sdk 14
|
||||
implementation 'com.android.support:support-core-utils:25.0.0'
|
||||
}
|
|
@ -1,20 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools" tools:ignore="ProtectedPermissions, ScopedStorage">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.READ_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="com.playstation.psstore.permission" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/app_icon"
|
||||
android:label="@string/app_name"
|
||||
tools:targetApi="13">
|
||||
<activity android:name="com.psmreborn.nopsmdrm.MainActivity">
|
||||
android:label="@string/app_name">
|
||||
<activity android:name="com.psmreborn.nopsmdrm.MainActivity" android:label="@string/name_nopsmdrm">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:launchMode="singleTop" android:name="com.psmreborn.nops1drm.LicenseCheckActivity" android:label="@string/name_nops1drm_ticket" android:icon="@drawable/nops1drm_ticket_icon" android:screenOrientation="landscape">
|
||||
<intent-filter>
|
||||
<action android:name="com.playstation.android.intent.action.CHECK_ENTITLEMENT"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.psmreborn.nops1drm.DownloadActivity" android:label="@string/name_nops1drm_update" android:icon="@drawable/nops1drm_update_icon" android:screenOrientation="landscape" android:windowSoftInputMode="stateAlwaysHidden">
|
||||
<intent-filter>
|
||||
<action android:name="com.playstation.android.intent.action.DOWNLOAD_ZPAK"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,142 @@
|
|||
package com.psmreborn.nops1drm;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.nopsmdrm.MainActivity;
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
import com.psmreborn.shared.Downloader;
|
||||
import com.psmreborn.shared.Logger;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class DownloadActivity extends Activity {
|
||||
private static final int RESPONSE_CODE = 20000305;
|
||||
private String entitlementId = null;
|
||||
private String appName = null;
|
||||
private boolean forceDownload = false;
|
||||
private String skuId = null;
|
||||
private String returnToActivity = null;
|
||||
private String returnToPackage = null;
|
||||
private String zpakLocation = null;
|
||||
@SuppressLint("WrongConstant")
|
||||
public void respond(int resp){
|
||||
Log.i("ZpakDl", "Responding with response code: "+resp);
|
||||
Intent resIntent = new Intent();
|
||||
this.setResult(resp, resIntent);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
private void forwardIntentToPsm(){
|
||||
Log.i("ZpakDl", "Forwarding intent to PSM ...");
|
||||
|
||||
Intent respondIntent = new Intent("com.playstation.android.intent.action.DOWNLOAD_ZPAK");
|
||||
respondIntent.putExtra("entitlement_id", this.entitlementId);
|
||||
respondIntent.putExtra("app_name", this.appName);
|
||||
respondIntent.putExtra("force_download", this.forceDownload);
|
||||
if(this.returnToPackage != null)
|
||||
respondIntent.putExtra("return_to_package", this.returnToPackage);
|
||||
|
||||
if(this.returnToActivity != null)
|
||||
respondIntent.putExtra("return_to_activity", this.returnToActivity);
|
||||
|
||||
if(this.skuId != null)
|
||||
respondIntent.putExtra("sku_id", this.skuId);
|
||||
|
||||
respondIntent.setPackage(this.getResources().getString(R.string.psm_app_package_id));
|
||||
|
||||
startActivityForResult(respondIntent, RESPONSE_CODE);
|
||||
}
|
||||
private boolean checkZpakExist(){
|
||||
Log.i("ZpakDl", "Checking for zpak file.");
|
||||
|
||||
File zpakLoc = new File(this.zpakLocation);
|
||||
if(!zpakLoc.exists()) return false;
|
||||
|
||||
String[] zpaks = zpakLoc.list();
|
||||
if(zpaks == null) return false;
|
||||
|
||||
for(String zpak : zpaks){
|
||||
Log.i("ZpakDl", "Chceking file: "+zpak);
|
||||
if(zpak.endsWith(".zpak")){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
protected void onActivityResult(int res, int res_code, Intent intent) {
|
||||
if(res == RESPONSE_CODE) {
|
||||
Logger.logText( "RES", String.valueOf(res_code));
|
||||
|
||||
if(res_code == 0) {
|
||||
if(checkZpakExist()){
|
||||
Log.i("ZpakDownload", "Update failed, faking sucess code.");
|
||||
res_code = -1;
|
||||
}
|
||||
}
|
||||
|
||||
respond(res_code);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Intent intent = this.getIntent();
|
||||
this.entitlementId = intent.getStringExtra("entitlement_id");
|
||||
this.appName = intent.getStringExtra("app_name");
|
||||
this.returnToPackage = intent.getStringExtra("return_to_package");
|
||||
this.returnToActivity = intent.getStringExtra("return_to_activity");
|
||||
this.forceDownload = intent.getBooleanExtra("force_download", false);
|
||||
this.skuId = intent.getStringExtra("sku_id");
|
||||
this.zpakLocation = new File(new File(new File(new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"), this.returnToPackage), "files"), "content").getAbsolutePath();
|
||||
|
||||
Logger.logText("entitlement_id", this.entitlementId);
|
||||
Logger.logText("app_name", this.appName);
|
||||
Logger.logText("return_to_package", this.returnToPackage);
|
||||
Logger.logText("return_to_activity", this.returnToActivity);
|
||||
Logger.logText("force_download", String.valueOf(this.forceDownload));
|
||||
Logger.logText("sku_id", this.skuId);
|
||||
Logger.logText("zpak_location", this.zpakLocation);
|
||||
|
||||
Handler handler = new Handler(this.getMainLooper());
|
||||
|
||||
new Thread(() -> {
|
||||
if(!checkZpakExist()){
|
||||
handler.post(() -> {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("zPAK not found!")
|
||||
.setMessage("No zPAK was found in:\n\""+this.zpakLocation+"\"\nI can start PSM to download it\nOr i could just tell "+this.appName+" to completely ignore it\n(this will probably crash the game)")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Launch PSM.", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
forwardIntentToPsm();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Ignore it.", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
respond(-1);
|
||||
}
|
||||
}).show();
|
||||
});
|
||||
|
||||
}
|
||||
else{
|
||||
handler.post(() -> {
|
||||
respond(-1);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package com.psmreborn.nops1drm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
import com.psmreborn.shared.Logger;
|
||||
import com.psmreborn.shared.Root;
|
||||
|
||||
public class LicenseCheckActivity extends Activity {
|
||||
private static final int RESPONSE_CODE = 19941203;
|
||||
private String entitlementId = null;
|
||||
private byte[] inputData = null;
|
||||
private ProgressDialog dialog = null;
|
||||
|
||||
public void respond(byte[] response, int resp){
|
||||
Intent resIntent = new Intent();
|
||||
resIntent.putExtra("result_data", response);
|
||||
this.setResult(resp, resIntent);
|
||||
this.dialog.dismiss();
|
||||
this.finish();
|
||||
}
|
||||
@Override
|
||||
protected void onActivityResult(int res, int res_code, Intent intent) {
|
||||
if(res == RESPONSE_CODE) {
|
||||
byte[] result_data = intent.getByteArrayExtra("result_data");
|
||||
Logger.logBytes("RESULT_DATA", result_data);
|
||||
Logger.logText( "RES", String.valueOf(res_code));
|
||||
respond(result_data, res_code);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void forwardIntentToPsm() {
|
||||
Log.i("TicketGen", "Forwarding intent to PSM ...");
|
||||
|
||||
Intent intent = new Intent("com.playstation.android.intent.action.CHECK_ENTITLEMENT");
|
||||
intent.putExtra("entitlement_id", this.entitlementId);
|
||||
intent.putExtra("input_data", this.inputData);
|
||||
intent.setPackage(this.getResources().getString(R.string.psm_app_package_id));
|
||||
|
||||
startActivityForResult(intent, RESPONSE_CODE);
|
||||
}
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Read intent from pss game
|
||||
this.entitlementId = this.getIntent().getStringExtra("entitlement_id");
|
||||
this.inputData = this.getIntent().getByteArrayExtra("input_data");
|
||||
|
||||
this.dialog = new ProgressDialog(this);
|
||||
this.dialog.setTitle("Generating NP Ticket ...");
|
||||
this.dialog.setMessage("Faking entitlement: "+this.entitlementId);
|
||||
this.dialog.setIndeterminate(true);
|
||||
this.dialog.setCancelable(false);
|
||||
this.dialog.show();
|
||||
|
||||
Logger.logText("ENTITLEMENT_ID", this.entitlementId);
|
||||
Logger.logBytes("INPUT_DATA", this.inputData);
|
||||
|
||||
// Generate ticket and put it into com.playstation.psstore databases.
|
||||
|
||||
Handler handler = new Handler(this.getMainLooper());
|
||||
|
||||
new Thread(()->{
|
||||
try {
|
||||
if(Root.init(this)){
|
||||
PsmStartupCache.addEntitlement(this, this.entitlementId);
|
||||
|
||||
handler.post(() ->{
|
||||
this.forwardIntentToPsm();
|
||||
});
|
||||
}
|
||||
else{
|
||||
handler.post(() -> {
|
||||
respond(new byte[]{}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e("LicenseCheckerActivity", e.toString());
|
||||
handler.post(() -> {
|
||||
respond(new byte[]{}, 0);
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package com.psmreborn.nops1drm;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.shared.Logger;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.util.ArrayList;
|
||||
|
||||
|
||||
import yuv.pink.npticket.BrokenTicketException;
|
||||
import yuv.pink.npticket.Cipher;
|
||||
import yuv.pink.npticket.Entitlement;
|
||||
import yuv.pink.npticket.NPDate;
|
||||
import yuv.pink.npticket.NPTicket;
|
||||
import yuv.pink.npticket.Subject;
|
||||
|
||||
public class NpTicketHlp {
|
||||
private static KeyPair generateRsaKeys() throws Exception {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
|
||||
kpg.initialize(1536);
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
private static KeyPair generateEcKeys() throws Exception {
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
|
||||
kpg.initialize(new ECGenParameterSpec("secp521r1"), new SecureRandom());
|
||||
return kpg.generateKeyPair();
|
||||
}
|
||||
public static boolean ticketExpired(byte[] ticket) {
|
||||
try {
|
||||
NPTicket nptik = new NPTicket();
|
||||
nptik.parse(ticket, new Cipher(0x79F97BD), false);
|
||||
return nptik.isExpired();
|
||||
} catch (IOException e){
|
||||
return true;
|
||||
} catch (BrokenTicketException e) {
|
||||
return true;
|
||||
} catch (Exception e){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] generateTicket(String entitlementId, String language, String onlineId, Long accountId) throws Exception {
|
||||
NPTicket ticket = new NPTicket();
|
||||
|
||||
String serviceId = entitlementId.substring(0x0, 0x13);
|
||||
|
||||
Log.i("TicketGen", "Generating ticket... ("+serviceId+") ("+entitlementId+")");
|
||||
|
||||
// set ticket haeder
|
||||
ticket.majorVersion = 4;
|
||||
ticket.minorVersion = 0;
|
||||
ticket.packetID = 1;
|
||||
ticket.partialPacket = false;
|
||||
ticket.jumboPacket = false;
|
||||
|
||||
// set ticket serial ..
|
||||
Log.i("TicketGen", "Generating serial number...");
|
||||
ticket.serial = new byte[0x14];
|
||||
new SecureRandom().nextBytes(ticket.serial);
|
||||
Logger.logBytes("TICKET_SERIAL", ticket.serial);
|
||||
|
||||
ticket.issuerID = 256;
|
||||
ticket.issuedDate = System.currentTimeMillis();
|
||||
ticket.notOnOrAfterDate = System.currentTimeMillis() + (1209600 * 1000);
|
||||
|
||||
Log.i("TicketGen", "Setting DOB...");
|
||||
|
||||
// set DOB
|
||||
NPDate npDob = new NPDate();
|
||||
npDob.year = 1999;
|
||||
npDob.day = 1;
|
||||
npDob.month = 1;
|
||||
|
||||
// set subject
|
||||
Log.i("TicketGen", "Setting Subject...");
|
||||
ticket.subject = new Subject(accountId, onlineId,
|
||||
new byte[] { language.getBytes("UTF-8")[0], language.getBytes("UTF-8")[1], 0x00, 0x04 },
|
||||
"d7", serviceId, npDob, 0x19000200, 0);
|
||||
|
||||
|
||||
// set cookie
|
||||
ticket.cookie = null;
|
||||
|
||||
// set entitlement
|
||||
Log.i("TicketGen", "Setting Entitlements ...");
|
||||
ticket.entitlements = new ArrayList<Entitlement>();
|
||||
ticket.entitlements.add(new Entitlement(entitlementId, System.currentTimeMillis(), 0L, 0, 0, 0));
|
||||
|
||||
// set footer
|
||||
Log.i("TicketGen", "Setting Footer...");
|
||||
ticket.roles = null;
|
||||
ticket.platform = "\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000";
|
||||
ticket.consoleID = new byte[0x40];
|
||||
ticket.dontHaveRSASignature = false;
|
||||
|
||||
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
Log.i("TicketGen", "Writing ticket to memory stream...");
|
||||
|
||||
//TODO: fix ecdsa cipher, and actually generate a signature
|
||||
ticket.writeTo(stream, new Cipher(0x79F97BD));
|
||||
return stream.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package com.psmreborn.nops1drm;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
import com.psmreborn.shared.Helper;
|
||||
import com.psmreborn.shared.Logger;
|
||||
import com.psmreborn.shared.Root;
|
||||
import com.psmreborn.shared.StringEncryptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PsmStartupCache {
|
||||
private static StringEncryptor stringEncryptor = null;
|
||||
private static String sha256HexStr(String str) {
|
||||
String lowerStr = str.toLowerCase();
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
messageDigest.update(lowerStr.getBytes("UTF-8"));
|
||||
byte[] digest = messageDigest.digest();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte hexDigit : digest) {
|
||||
sb.append(Integer.toHexString(hexDigit & 255));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private static void copyToPsm(Context ctx) throws Exception {
|
||||
String psmDataDir = Helper.getPsmAppInfo(ctx).dataDir;
|
||||
String psmDatabasesDir = new File(psmDataDir, "databases").getAbsolutePath();
|
||||
String psmStartContentDb = new File(psmDatabasesDir, "start_content.db").getAbsolutePath();
|
||||
|
||||
String myDataDir = Helper.getAppInfo(ctx, ctx.getPackageName()).dataDir;
|
||||
String myDatabasesDir = new File(myDataDir, "databases").getAbsolutePath();
|
||||
String myStartContentDb = new File(myDatabasesDir, "start_content.db").getAbsolutePath();
|
||||
|
||||
Root.mkdirAndChmodChown(psmDatabasesDir, 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
Root.copyChmodAndChown(myStartContentDb, psmStartContentDb, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
private static void copyFromPsm(Context ctx) throws Exception {
|
||||
String psmDataDir = Helper.getPsmAppInfo(ctx).dataDir;
|
||||
String psmDatabasesDir = new File(psmDataDir, "databases").getAbsolutePath();
|
||||
String psmStartContentDb = new File(psmDatabasesDir, "start_content.db").getAbsolutePath();
|
||||
|
||||
String myDataDir = Helper.getAppInfo(ctx, ctx.getPackageName()).dataDir;
|
||||
String myDatabasesDir = new File(myDataDir, "databases").getAbsolutePath();
|
||||
String myStartContentDb = new File(myDatabasesDir, "start_content.db").getAbsolutePath();
|
||||
|
||||
Root.mkdirAndChmodChown(myDatabasesDir, 771, String.valueOf(Helper.getMyUid(ctx)));
|
||||
Root.copyChmodAndChown(psmStartContentDb, myStartContentDb, 660, String.valueOf(Helper.getMyUid(ctx)));
|
||||
}
|
||||
private static int dbRemoveEntry(StartContentDb cdb, String entitlementId) {
|
||||
Logger.logText("DELETE, ENTITLEMENT_ID", entitlementId);
|
||||
SQLiteDatabase db = cdb.getWritableDatabase();
|
||||
int v = db.delete("cache_table", "entitlement_id = ?", new String[]{entitlementId});
|
||||
db.close();
|
||||
return v;
|
||||
}
|
||||
|
||||
private static byte[] dbGetEntry(StartContentDb cdb, String entitlementId){
|
||||
Log.i("TicketGen", "dbGetEntry, "+entitlementId);
|
||||
SQLiteDatabase db = cdb.getReadableDatabase();
|
||||
Cursor query = db.query("cache_table", new String[] {"_id", "user_id", "entitlement_id", "ticket_data", "last_update"}, "entitlement_id = ?", new String[]{entitlementId}, null, null, null);
|
||||
if (query == null || query.getCount() != 1) {
|
||||
db.close();
|
||||
return null;
|
||||
} else {
|
||||
query.moveToFirst();
|
||||
byte[] ticketEnc = query.getBlob(query.getColumnIndex("ticket_data"));
|
||||
query.close();
|
||||
db.close();
|
||||
return stringEncryptor.decrypt(ticketEnc);
|
||||
}
|
||||
|
||||
}
|
||||
private static void dbAddEntry(StartContentDb cdb, String userId, String entitlementId, byte[] ticket) throws Exception {
|
||||
Log.i("TicketGen", "dbAddEntry, "+entitlementId);
|
||||
|
||||
SQLiteDatabase db = cdb.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
byte[] encryptedTicket = stringEncryptor.encrypt(ticket);
|
||||
String shaUserId = sha256HexStr(userId);
|
||||
|
||||
values.put("user_id", shaUserId);
|
||||
values.put("entitlement_id", entitlementId);
|
||||
values.put("ticket_data", encryptedTicket);
|
||||
values.put("last_update", System.currentTimeMillis());
|
||||
|
||||
Logger.logText("USER_ID", shaUserId);
|
||||
Logger.logText("ENTITLEMENT_ID", entitlementId);
|
||||
Logger.logBytes("TICKET_DATA", ticket);
|
||||
Logger.logBytes("ENCRYPTED_TICKET_DATA", encryptedTicket);
|
||||
|
||||
Long pos = db.insert("cache_table", null, values);
|
||||
|
||||
db.close();
|
||||
|
||||
if(pos <= -1)
|
||||
throw new Exception("failed to insert");
|
||||
}
|
||||
|
||||
|
||||
public static void addEntitlement(Context ctx, String entitlement_id) throws Exception {
|
||||
stringEncryptor = new StringEncryptor(Helper.getAndroidIdOfPsm(ctx), Helper.getPsmUid(ctx));
|
||||
try{copyFromPsm(ctx);} catch (Exception ignored) { };
|
||||
|
||||
// get default values for onlineid, accountid, etc.
|
||||
String email = stringEncryptor.decryptString(Helper.getSharedPrefFromPsm(ctx, "SigninInfo", "SignedInUsername"), ctx.getResources().getString(R.string.default_email));
|
||||
Long accountId = Long.valueOf(stringEncryptor.decryptString(Helper.getSharedPrefFromPsm(ctx, "com.playstation.psstore_preferences", "last_signin_account_id"), String.valueOf(Helper.getDefaultAccountId(ctx))));
|
||||
String onlineId = ctx.getResources().getString(R.string.default_online_id);
|
||||
|
||||
// read psm start content.db file ..
|
||||
StartContentDb scdb = new StartContentDb(ctx);
|
||||
byte[] ticket = dbGetEntry(scdb, entitlement_id);
|
||||
if(ticket == null){
|
||||
Log.i("TicketGen", "No ticket found, generating new!");
|
||||
dbAddEntry(scdb, email, entitlement_id, NpTicketHlp.generateTicket(entitlement_id, Locale.getDefault().getLanguage(), onlineId, accountId));
|
||||
}
|
||||
else{
|
||||
if(NpTicketHlp.ticketExpired(ticket)) {
|
||||
Log.i("TicketGen", "Previous ticket expired, generating new one!");
|
||||
dbRemoveEntry(scdb, entitlement_id);
|
||||
dbAddEntry(scdb, email, entitlement_id, NpTicketHlp.generateTicket(entitlement_id, Locale.getDefault().getLanguage(), onlineId, accountId));
|
||||
}
|
||||
else{
|
||||
Log.i("TicketGen", "Existing ticket is all good! continuing to PSM!");
|
||||
}
|
||||
}
|
||||
|
||||
scdb.close();
|
||||
|
||||
try{copyToPsm(ctx);} catch (Exception ignored) { };
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.psmreborn.nops1drm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
final class StartContentDb extends SQLiteOpenHelper {
|
||||
public StartContentDb(Context context) {
|
||||
this(context, "start_content.db");
|
||||
}
|
||||
|
||||
private StartContentDb(Context context, String str) {
|
||||
super(context, str, (SQLiteDatabase.CursorFactory) null, 1);
|
||||
}
|
||||
|
||||
@Override // android.database.sqlite.SQLiteOpenHelper
|
||||
public final void onCreate(SQLiteDatabase sqllite) {
|
||||
sqllite.execSQL("CREATE TABLE IF NOT EXISTS cache_table (_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,user_id TEXT,entitlement_id TEXT NOT NULL,ticket_data BLOB NOT NULL,last_update LONG NOT NULL)");
|
||||
}
|
||||
|
||||
@Override // android.database.sqlite.SQLiteOpenHelper
|
||||
public final void onUpgrade(SQLiteDatabase sqllite, int i, int i2) {
|
||||
sqllite.execSQL("DROP TABLE IF EXISTS cache_table");
|
||||
sqllite.execSQL("CREATE TABLE IF NOT EXISTS cache_table (_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,user_id TEXT,entitlement_id TEXT NOT NULL,ticket_data BLOB NOT NULL,last_update LONG NOT NULL)");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.shared.Helper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class DumpAllRifs {
|
||||
public static void setDumpAllFlagAndStartPsm(Activity ctx) {
|
||||
ProgressDialog dialog = new ProgressDialog(ctx);
|
||||
dialog.setTitle("Starting PSM ...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setCancelable(false);
|
||||
dialog.show();
|
||||
|
||||
new Thread(() -> {
|
||||
Handler handler = new Handler(ctx.getMainLooper());
|
||||
File psmFolder = new File(Environment.getExternalStorageDirectory(), "psm");
|
||||
File psmDumpAllFlagFile = new File(psmFolder, "dump_all");
|
||||
File psmAndroidFolder = new File(new File(new File(new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"), "com.playstation.psstore"), "files"), "psm");
|
||||
|
||||
// stop psm
|
||||
Helper.killPsm(ctx);
|
||||
|
||||
try {
|
||||
psmDumpAllFlagFile.createNewFile();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get a games title id..
|
||||
String titleId = "";
|
||||
String[] allGames = psmAndroidFolder.list();
|
||||
for(String chkTitleId : allGames) {
|
||||
File appInfoFile = new File(new File(new File(psmAndroidFolder, chkTitleId), "Application"), "app.info");
|
||||
if(appInfoFile.exists()){
|
||||
titleId = chkTitleId;
|
||||
}
|
||||
}
|
||||
|
||||
if(titleId.equals("")){
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d("DUMPALLRIFS", "Using TitleID: "+titleId);
|
||||
|
||||
WifiManager wifiManager = (WifiManager) ctx.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
||||
if(wifiManager != null){
|
||||
Intent intent = new Intent("com.playstation.psm.intent.action.START_CONTENT");
|
||||
intent.putExtra("titleId", titleId);
|
||||
intent.setData(Uri.parse("psgmpsm:play?titleId="+titleId));
|
||||
|
||||
handler.post(() -> {
|
||||
// disable wifi
|
||||
wifiManager.setWifiEnabled(false);
|
||||
|
||||
// start psm ...
|
||||
ctx.startActivity(intent);
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
else{
|
||||
return;
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.psmreborn.shared.Helper;
|
||||
import com.psmreborn.shared.Root;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FixPermissions {
|
||||
public static void changePermissionsToPsm(Activity ctx) {
|
||||
ProgressDialog dialog = new ProgressDialog(ctx);
|
||||
dialog.setTitle("Fixing permissions ...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setCancelable(false);
|
||||
dialog.show();
|
||||
|
||||
new Thread(() -> {
|
||||
Handler handler = new Handler(ctx.getMainLooper());
|
||||
|
||||
Helper.killPsm(ctx);
|
||||
String androidDataFolder = new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data").getAbsolutePath();
|
||||
try {
|
||||
String groupId = Root.getFileGroup(androidDataFolder);
|
||||
String userId = String.valueOf(Helper.getPsmAppInfo(ctx).uid);
|
||||
Root.chownRoot(new File(new File(new File(androidDataFolder, "com.playstation.psstore"), "files"), "psm").getAbsolutePath(), userId, groupId);
|
||||
|
||||
handler.post(() -> {
|
||||
dialog.dismiss();
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Success!")
|
||||
.setMessage("Successfully updated file permissions")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", null).show();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
handler.post(() -> {
|
||||
dialog.dismiss();
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Failed.")
|
||||
.setMessage(e.toString())
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", null).show();
|
||||
});
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Helper {
|
||||
|
||||
private static Context ctx;
|
||||
public static void setContext(Context context){
|
||||
ctx = context;
|
||||
}
|
||||
public static boolean isPsmInstalled(){
|
||||
try {
|
||||
ctx.getPackageManager().getApplicationInfo("com.playstation.psstore", 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static ApplicationInfo getPsmApp() throws PackageManager.NameNotFoundException {
|
||||
ApplicationInfo pkg = ctx.getPackageManager().getApplicationInfo("com.playstation.psstore", 0);
|
||||
return pkg;
|
||||
}
|
||||
public static String getDateTime(){
|
||||
String formattedDate = new SimpleDateFormat("MM_dd_yyyy_HH_mm_ss").format(new Date());
|
||||
return formattedDate;
|
||||
}
|
||||
}
|
|
@ -1,23 +1,28 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.View;
|
||||
import com.psmreborn.pscertified.PlayStationCertified;
|
||||
import com.psmreborn.pscertified.PsCertificatesInstaller;
|
||||
import com.psmreborn.shared.Downloader;
|
||||
import com.psmreborn.shared.Helper;
|
||||
|
||||
import com.psmreborn.nopsmdrm.pscertified.PlayStationCertified;
|
||||
import com.psmreborn.nopsmdrm.pscertified.PsCertificatesInstaller;
|
||||
import java.io.File;
|
||||
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
Helper.setContext(this);
|
||||
|
||||
(new Startup(this)).execute();
|
||||
}
|
||||
|
@ -25,29 +30,87 @@ public class MainActivity extends Activity {
|
|||
@Override
|
||||
public void onBackPressed(){
|
||||
finish();
|
||||
System.exit(0);
|
||||
super.onBackPressed();
|
||||
}
|
||||
public void installStart(View view) {
|
||||
PlayStationCertified playStationCertified = new PlayStationCertified(this);
|
||||
if(!playStationCertified.isPlaystationCertified()){
|
||||
new AlertDialog.Builder((Activity)this)
|
||||
.setTitle("PS Certification Missing")
|
||||
.setMessage("Your device appears to not be\"PlayStation Certified\"\nDo you want to certify it?\n(Warning: modifies /system/)")
|
||||
|
||||
private void downloadPsmAlert() {
|
||||
new AlertDialog.Builder((Activity)this)
|
||||
.setTitle("PSM App not found!")
|
||||
.setMessage("Would you like to download and install the PlayStation Mobile app?")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Downloader.downloadAndInstall(MainActivity.this,
|
||||
"http://psmreborn.com/psm-android/Psm1.7.0.apk");
|
||||
}
|
||||
})
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
private void psCertifiedAlert(){
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("PS Certification Missing")
|
||||
.setMessage((Build.VERSION.SDK_INT >= 23) ? "Your device appears to not be \"PlayStation Certified\"\nOn android 6+ this requires installing a Magisk module\nWould you like to install \"MagiskCertify.zip\"" :
|
||||
"Your device appears to not be \"PlayStation Certified\"\nDo you want to install them?\n(Warning: modifies /system/)")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
(new PsCertificatesInstaller(MainActivity.this)).execute();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
|
||||
public void installStart(View view) {;
|
||||
if(Permissions.checkPermsAlert(this)){
|
||||
if(!PlayStationCertified.isPlaystationCertified(this.getApplicationContext())){
|
||||
psCertifiedAlert();
|
||||
}
|
||||
else if(!Helper.isPsmInstalled(this)) {
|
||||
downloadPsmAlert();
|
||||
}
|
||||
else {
|
||||
new NoPsmDrmInstaller(this).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dumpAllRifs(View view){
|
||||
if(Permissions.checkPermsAlert(this)){
|
||||
String rifOutput = new File(Environment.getExternalStorageDirectory(), "psm").getAbsolutePath();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Backup licenses")
|
||||
.setMessage("This will show a black screen for awhile\nthen a PlayStation Mobile game will start\nOnce the game starts, a backup of all your PSM licenses can be found at\n\""+rifOutput+"\"\nDo you want to do this?")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
(new PsCertificatesInstaller(MainActivity.this)).execute();
|
||||
DumpAllRifs.setDumpAllFlagAndStartPsm(MainActivity.this);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("No", new DialogInterface.OnClickListener() {
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
|
||||
}
|
||||
public void givePsmOwnership(View view){
|
||||
if(Permissions.checkPermsAlert(this)){
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Fix permissions")
|
||||
.setMessage("On android 10+, games wont always show up in PSM when placed in \"com.playstation.psstore\" folder due to file permissions.\nWould you like to fix all the file permissions for PSM Games? (can take awhile)")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
(new NoPsmDrmInstaller(MainActivity.this)).execute();
|
||||
FixPermissions.changePermissionsToPsm(MainActivity.this);
|
||||
}
|
||||
}).show();
|
||||
}
|
||||
else {
|
||||
(new NoPsmDrmInstaller(this)).execute();
|
||||
})
|
||||
.setNegativeButton("No", null).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -1,25 +1,34 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import static com.psmreborn.nopsmdrm.Helper.*;
|
||||
import static com.psmreborn.nopsmdrm.Root.*;
|
||||
import com.psmreborn.nopsmdrm.pscertified.PlayStationCertified;
|
||||
import static com.psmreborn.shared.Helper.*;
|
||||
import static com.psmreborn.shared.Root.*;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.psmreborn.shared.Files;
|
||||
import com.psmreborn.shared.Helper;
|
||||
import com.psmreborn.shared.Root;
|
||||
import com.psmreborn.shared.StringEncryptor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.chainfire.libsuperuser.Shell;
|
||||
|
||||
|
@ -30,11 +39,12 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
private String errorMsg = "";
|
||||
private ProgressDialog dialog = null;
|
||||
private Context ctx = null;
|
||||
private StringEncryptor stringEncryptor = null;
|
||||
private Handler handler = null;
|
||||
|
||||
private StringEncryptor encryptor = null;
|
||||
public NoPsmDrmInstaller(Context context){
|
||||
this.ctx = context;
|
||||
this.stringEncryptor = new StringEncryptor(this.ctx);
|
||||
this.handler = new Handler(ctx.getMainLooper());
|
||||
}
|
||||
|
||||
private void setError(String msg) throws Exception {
|
||||
|
@ -44,9 +54,8 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
throw new Exception(msg);
|
||||
}
|
||||
|
||||
private void generateDeviceFingerprint(String outputFilename) throws FileNotFoundException, UnsupportedEncodingException {
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private void generateDeviceFingerprint(String outputFilename) throws Exception {
|
||||
TelephonyManager tm = ((TelephonyManager) ctx.getSystemService( Context.TELEPHONY_SERVICE));
|
||||
|
||||
String deviceId = "(blank)";
|
||||
|
@ -85,8 +94,8 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
type = "(blank)";
|
||||
|
||||
PrintWriter writer = new PrintWriter(outputFilename, "UTF-8");
|
||||
writer.println(String.valueOf(stringEncryptor.getPsmUid()));
|
||||
writer.println(stringEncryptor.getAndroidId());
|
||||
writer.println(String.valueOf(Helper.getPsmUid(ctx)));
|
||||
writer.println(Helper.getAndroidIdOfPsm(ctx));
|
||||
writer.println(deviceId);
|
||||
writer.println(serial);
|
||||
writer.println(brand);
|
||||
|
@ -98,120 +107,276 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
writer.close();
|
||||
}
|
||||
|
||||
private void backupPsm() throws Exception {
|
||||
|
||||
String psmFilesDir = new File(getPsmApp().dataDir, "files").getAbsolutePath();
|
||||
|
||||
private void backupPsm() throws Exception {
|
||||
String psmFilesDir = new File(Helper.getPsmAppInfo(ctx).dataDir, "files").getAbsolutePath();
|
||||
String psmKdcDir = new File(psmFilesDir, "kdc").getAbsolutePath();
|
||||
String psmActFile = new File(psmKdcDir, "act.dat").getAbsolutePath();
|
||||
String psmAndroidFolder = new File(new File(new File(new File(new File(Environment.getExternalStorageDirectory(), "Android"), "data"), "com.playstation.psstore"), "files"), "psm").getAbsolutePath();
|
||||
String psmSdcardFolder = new File(Environment.getExternalStorageDirectory(), "psm").getAbsolutePath();
|
||||
|
||||
// check if "act.dat" exists -- they've been here before ...
|
||||
if(fileExistRoot(psmActFile)) {
|
||||
String psmSdcardFolder = new File(Environment.getExternalStorageDirectory(), "psm").getAbsolutePath();
|
||||
String devinfoFile = new File(ctx.getCacheDir(), "devinfo.txt").getAbsolutePath();
|
||||
String tarFilename = new File(psmSdcardFolder, "psm_"+getDateTime() + ".tar").getAbsolutePath();
|
||||
|
||||
|
||||
// check if "act.dat" exists -- they've used PSM before ...
|
||||
if(Root.fileExistRoot(psmActFile)) {
|
||||
new File(psmSdcardFolder).mkdirs();
|
||||
String filename = "psm_"+getDateTime();
|
||||
|
||||
// tar up the files
|
||||
tarRoot(getPsmApp().dataDir,new File(psmSdcardFolder, filename+".tar").getAbsolutePath());
|
||||
File psmAndroidFile = new File(psmAndroidFolder);
|
||||
|
||||
// generate device fingerprint file
|
||||
generateDeviceFingerprint(new File(psmSdcardFolder, filename + "_DEV.txt").getAbsolutePath());
|
||||
generateDeviceFingerprint(devinfoFile);
|
||||
|
||||
ArrayList<String> filesToTar = new ArrayList<String>();
|
||||
filesToTar.add(Helper.getPsmAppInfo(ctx).dataDir);
|
||||
filesToTar.add(devinfoFile);
|
||||
|
||||
// add all license folders ....
|
||||
if(psmAndroidFile.exists()){
|
||||
for(String gameFolder : psmAndroidFile.list()){
|
||||
File licenseFolder = new File(new File(psmAndroidFile, gameFolder), "License");
|
||||
if(licenseFolder.exists()){
|
||||
filesToTar.add(licenseFolder.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tar up the files
|
||||
tarRoot(filesToTar, tarFilename);
|
||||
|
||||
handler.post(() -> {
|
||||
Toast.makeText(ctx.getApplicationContext(),"Backed up existing PSM Data to:\n\""+tarFilename+"\"", Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void makeDirs() throws PackageManager.NameNotFoundException, Shell.ShellDiedException {
|
||||
mkdirAndChmodChown(new File(getPsmApp().dataDir, "cache").getAbsolutePath(), 771, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
mkdirAndChmodChown(new File(getPsmApp().dataDir, "shared_prefs").getAbsolutePath(), 771, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
mkdirAndChmodChown(new File(getPsmApp().dataDir, "files").getAbsolutePath(), 771, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
mkdirAndChmodChown(new File(new File(getPsmApp().dataDir, "files"), "kdc").getAbsolutePath(), 771, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
mkdirAndChmodChown(new File(getPsmApp().dataDir, "databases").getAbsolutePath(), 771, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
mkdirAndChmodChown(new File(Helper.getPsmAppInfo(ctx).dataDir, "cache").getAbsolutePath(), 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
mkdirAndChmodChown(new File(Helper.getPsmAppInfo(ctx).dataDir, "shared_prefs").getAbsolutePath(), 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
mkdirAndChmodChown(new File(Helper.getPsmAppInfo(ctx).dataDir, "files").getAbsolutePath(), 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
mkdirAndChmodChown(new File(new File(Helper.getPsmAppInfo(ctx).dataDir, "files"), "kdc").getAbsolutePath(), 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
mkdirAndChmodChown(new File(Helper.getPsmAppInfo(ctx).dataDir, "databases").getAbsolutePath(), 771, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
|
||||
private void generateWorkaroundSharedPrefs(String sharedPrefsPath) throws Exception {
|
||||
String emailAddress = encryptor.encryptString(ctx.getResources().getString(R.string.default_email));
|
||||
String password = encryptor.encryptString(ctx.getResources().getString(R.string.default_password));
|
||||
|
||||
// get the cache folder for our 'shared_prefs'
|
||||
String tmpPrefsFolder = ctx.getCacheDir().getAbsolutePath();
|
||||
|
||||
String mySigninInfo = new File(tmpPrefsFolder, "SigninInfo.xml").getAbsolutePath();
|
||||
String psmSigninInfo = new File(sharedPrefsPath, "SigninInfo.xml").getAbsolutePath();
|
||||
|
||||
// generate shared_prefs
|
||||
Files.writeTxtFile(mySigninInfo, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
|
||||
"<map>\n<string name=\"SignedInUsername\">"+emailAddress+"</string>\n" +
|
||||
"<boolean name=\"PassSave\" value=\"true\" />\n" +
|
||||
"<string name=\"Password\">"+password+"</string>\n" +
|
||||
"<boolean name=\"AutoSignIn\" value=\"true\" />\n" +
|
||||
"</map>\n");
|
||||
|
||||
copyChmodAndChown(mySigninInfo, psmSigninInfo, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
private void generateSharedPrefs(String sharedPrefsPath) throws Exception {
|
||||
|
||||
// encrypt the actual strings, username, password, etc
|
||||
String emailAddress = encryptor.encryptString(ctx.getResources().getString(R.string.default_email));
|
||||
String password = encryptor.encryptString(ctx.getResources().getString(R.string.default_password));
|
||||
String accountId = encryptor.encryptString(String.valueOf(Long.valueOf(ctx.getResources().getString(R.string.default_account_id), 16)));
|
||||
|
||||
// get the cache folder for our 'shared_prefs'
|
||||
String tmpPrefsFolder = ctx.getCacheDir().getAbsolutePath();
|
||||
|
||||
// work out paths to each file ...
|
||||
String mySigninInfo = new File(tmpPrefsFolder, "SigninInfo.xml").getAbsolutePath();
|
||||
String myPsstorePrefs = new File(tmpPrefsFolder, "com.playstation.psstore_preferences.xml").getAbsolutePath();
|
||||
String myRunningContentInfo = new File(tmpPrefsFolder, "RunningContentInfo.xml").getAbsolutePath();
|
||||
String myLocalLibrary = new File(tmpPrefsFolder, "LocalLibrary.xml").getAbsolutePath();
|
||||
|
||||
String psmSigninInfo = new File(sharedPrefsPath, "SigninInfo.xml").getAbsolutePath();
|
||||
String psmPsstorePrefs = new File(sharedPrefsPath, "com.playstation.psstore_preferences.xml").getAbsolutePath();
|
||||
String psmRunningContentInfo = new File(sharedPrefsPath, "RunningContentInfo.xml").getAbsolutePath();
|
||||
String psmLocalLibrary = new File(sharedPrefsPath, "LocalLibrary.xml").getAbsolutePath();
|
||||
|
||||
// generate shared_prefs
|
||||
Files.writeTxtFile(mySigninInfo, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
|
||||
"<map>\n<string name=\"SignedInUsername\">"+emailAddress+"</string>\n" +
|
||||
"<boolean name=\"PassSave\" value=\"true\" />\n" +
|
||||
"<string name=\"Password\">"+password+"</string>\n" +
|
||||
"<boolean name=\"AutoSignIn\" value=\"true\" />\n" +
|
||||
"</map>\n");
|
||||
|
||||
Files.writeTxtFile(myPsstorePrefs, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
|
||||
"<map>\n" +
|
||||
"<boolean name=\"key_upgradeDownloadTableForNeedWifi\" value=\"true\" />\n" +
|
||||
"<string name=\"last_signin_account_id\">"+accountId+"</string>\n" +
|
||||
"<long name=\"last_signin_account_region\" value=\"2\" />\n" +
|
||||
"<int name=\"key_psstore\" value=\"1\" />\n" +
|
||||
"<int name=\"key_downloader\" value=\"1\" />\n" +
|
||||
"<int name=\"psm_license_agree_version_code\" value=\"1170\" />\n" +
|
||||
"<int name=\"key_xmlcache\" value=\"1\" />\n" +
|
||||
"<string name=\"last_signin_account_country\">US</string>\n" +
|
||||
"<int name=\"key_startcontent\" value=\"1\" />\n<int name=\"key_nsxevent\" value=\"1\" />\n" +
|
||||
"<boolean name=\"key_upgradeLibraryTableForLocationUseConfirmationDate\" value=\"true\" />\n" +
|
||||
"<int name=\"key_install\" value=\"1\" />\n" +
|
||||
"<string name=\"update_md5\">387ce7e424258aef426aaa5be8a1638a</string>\n" +
|
||||
"<boolean name=\"psm_license_agree\" value=\"true\" />\n" +
|
||||
"<int name=\"key_guestinfo\" value=\"1\" />\n" +
|
||||
"<string name=\"last_signin_account_language\">"+ Locale.getDefault().getLanguage()+"</string>\n" +
|
||||
"<int name=\"key_cache\" value=\"2\" />\n" +
|
||||
"<int name=\"key_signinfo\" value=\"2\" />\n" +
|
||||
"</map>\n");
|
||||
|
||||
Files.writeTxtFile(myRunningContentInfo, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
|
||||
"<map>\n<null name=\"title_id\" />\n" +
|
||||
"<null name=\"next_title_id\" />\n" +
|
||||
"</map>\n");
|
||||
|
||||
Files.writeTxtFile(myLocalLibrary, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" +
|
||||
"<map>\n<boolean name=\"notDisplayAgain\" value=\"true\" />\n" +
|
||||
"<int name=\"sortType\" value=\"0\" />\n" +
|
||||
"<boolean name=\"isList\" value=\"false\" />\n" +
|
||||
"</map>\n");
|
||||
|
||||
// copy to the correct place and set permissions properly.
|
||||
copyChmodAndChown(mySigninInfo, psmSigninInfo, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
copyChmodAndChown(myPsstorePrefs, psmPsstorePrefs, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
copyChmodAndChown(myRunningContentInfo, psmRunningContentInfo, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
copyChmodAndChown(myLocalLibrary, psmLocalLibrary, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
|
||||
private boolean isNoPsmDrmAccountId() throws Exception {
|
||||
long accountId = Long.parseLong(encryptor.decryptString(Helper.getSharedPrefFromPsm(ctx, "com.playstation.psstore_preferences", "last_signin_account_id"), String.valueOf(getDefaultAccountId(ctx))));
|
||||
return accountId == getDefaultAccountId(ctx);
|
||||
}
|
||||
private void installSharedPrefs() throws Exception {
|
||||
// get the path to the shared_prefs folder
|
||||
String sharedPrefsPath = new File(getPsmApp().dataDir, "shared_prefs").getAbsolutePath();
|
||||
String sharedPrefsPath = new File(Helper.getPsmAppInfo(ctx).dataDir, "shared_prefs").getAbsolutePath();
|
||||
|
||||
// setup StringEncryptor
|
||||
String androidId = Helper.getAndroidIdOfPsm(ctx);
|
||||
this.encryptor = new StringEncryptor((androidId == null) ? ctx.getResources().getString(R.string.default_account_id) : androidId, Helper.getPsmUid(ctx));
|
||||
|
||||
// check if signininfo.xml file exists or not ...
|
||||
boolean signinInfoExist = fileExistRoot(new File(sharedPrefsPath, "SigninInfo.xml").getAbsolutePath());
|
||||
boolean isNoPsmDrmAccount = signinInfoExist && isNoPsmDrmAccountId();
|
||||
|
||||
if (!signinInfoExist) { // if file not found ...
|
||||
// then generate our own shared_prefs
|
||||
if (!signinInfoExist || isNoPsmDrmAccount) {
|
||||
// if no SigninInfo.xml file was not found
|
||||
// or if it was found, but it is using the default account id ...
|
||||
// then generate our own shared_prefs
|
||||
|
||||
// encrypt the actual strings, username, password, etc
|
||||
String emailAddress = stringEncryptor.encryptString("nopsmdrm@transrights.lgbt");
|
||||
String password = stringEncryptor.encryptString("password");
|
||||
String accountId = stringEncryptor.encryptString(String.valueOf(0x123456789ABCDEFL));
|
||||
|
||||
// get the cache folder for our 'shared_prefs'
|
||||
String tmpPrefsFolder = ctx.getCacheDir().getAbsolutePath();
|
||||
|
||||
// work out paths to each file ...
|
||||
String c_signinInfo = new File(tmpPrefsFolder, "SigninInfo.xml").getAbsolutePath();
|
||||
String c_psstorePrefs = new File(tmpPrefsFolder, "com.playstation.psstore_preferences.xml").getAbsolutePath();
|
||||
String c_runningContentInfo = new File(tmpPrefsFolder, "RunningContentInfo.xml").getAbsolutePath();
|
||||
String c_localLibrary = new File(tmpPrefsFolder, "LocalLibrary.xml").getAbsolutePath();
|
||||
|
||||
String r_signinInfo = new File(sharedPrefsPath, "SigninInfo.xml").getAbsolutePath();
|
||||
String r_psstorePrefs = new File(sharedPrefsPath, "com.playstation.psstore_preferences.xml").getAbsolutePath();
|
||||
String r_runningContentInfo = new File(sharedPrefsPath, "RunningContentInfo.xml").getAbsolutePath();
|
||||
String r_localLibrary = new File(sharedPrefsPath, "LocalLibrary.xml").getAbsolutePath();
|
||||
|
||||
// generate shared_prefs
|
||||
writeTxtFile(c_signinInfo, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map>\n<string name=\"SignedInUsername\">"+emailAddress+"</string>\n<boolean name=\"PassSave\" value=\"true\" />\n<string name=\"Password\">"+password+"</string>\n<boolean name=\"AutoSignIn\" value=\"true\" />\n</map>\n");
|
||||
writeTxtFile(c_psstorePrefs, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map>\n<boolean name=\"key_upgradeDownloadTableForNeedWifi\" value=\"true\" />\n<string name=\"last_signin_account_id\">"+accountId+"</string>\n<long name=\"last_signin_account_region\" value=\"2\" />\n<int name=\"key_psstore\" value=\"1\" />\n<int name=\"key_downloader\" value=\"1\" />\n<int name=\"psm_license_agree_version_code\" value=\"1170\" />\n<boolean name=\"key_notDisplayAgainEndOfSupportPreNavi\" value=\"true\" />\n<int name=\"key_xmlcache\" value=\"1\" />\n<string name=\"last_signin_account_country\">US</string>\n<boolean name=\"key_notDisplayAgainContentStartNavi\" value=\"true\" />\n<int name=\"key_startcontent\" value=\"1\" />\n<int name=\"key_nsxevent\" value=\"1\" />\n<boolean name=\"key_upgradeLibraryTableForLocationUseConfirmationDate\" value=\"true\" />\n<int name=\"key_install\" value=\"1\" />\n<string name=\"update_md5\">387ce7e424258aef426aaa5be8a1638a</string>\n<boolean name=\"psm_license_agree\" value=\"true\" />\n<int name=\"key_guestinfo\" value=\"1\" />\n<string name=\"last_signin_account_language\">en</string>\n<int name=\"key_cache\" value=\"2\" />\n<int name=\"key_signinfo\" value=\"2\" />\n</map>\n");
|
||||
writeTxtFile(c_runningContentInfo, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map>\n<null name=\"title_id\" />\n<null name=\"next_title_id\" />\n</map>\n");
|
||||
writeTxtFile(c_localLibrary, "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n<map>\n<boolean name=\"notDisplayAgain\" value=\"true\" />\n<int name=\"sortType\" value=\"0\" />\n<boolean name=\"isList\" value=\"false\" />\n</map>\n");
|
||||
|
||||
// copy to the correct place and set permissions properly.
|
||||
copyChmodAndChown(c_signinInfo, r_signinInfo, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
copyChmodAndChown(c_psstorePrefs, r_psstorePrefs, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
copyChmodAndChown(c_runningContentInfo, r_runningContentInfo, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
copyChmodAndChown(c_localLibrary, r_localLibrary, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
if(androidId != null){
|
||||
generateSharedPrefs(sharedPrefsPath);
|
||||
}
|
||||
else {
|
||||
generateWorkaroundSharedPrefs(sharedPrefsPath);
|
||||
Helper.killPsm(ctx);
|
||||
handler.post(() ->{
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Android 8+ android_id...")
|
||||
.setMessage("Could not obtain PSMs \"android_id\"\nPlease start the PSM app one time.\nthen run this application again!")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
System.exit(0);
|
||||
}
|
||||
}).show();
|
||||
});
|
||||
while(true) { Thread.sleep(1000); }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void patchLibdefault(String file) throws IOException {
|
||||
RandomAccessFile fs = new RandomAccessFile(file, "rw");
|
||||
|
||||
// change MD5_Init, MD5_Final and MD5_Update from import type GLOBAL to WEAK.
|
||||
|
||||
fs.seek(0xB240);
|
||||
fs.write(0x22);
|
||||
|
||||
fs.seek(0xBA70);
|
||||
fs.write(0x22);
|
||||
|
||||
fs.seek(0x130E0);
|
||||
fs.write(0x22);
|
||||
|
||||
|
||||
fs.close();
|
||||
}
|
||||
|
||||
private void installNoPsmDrmModules() throws Exception {
|
||||
String nativeLibsFolder = getPsmApp().nativeLibraryDir;
|
||||
String nativeLibsFolder = getPsmAppInfo(ctx).nativeLibraryDir;
|
||||
|
||||
String libPsmKdcFile = new File(nativeLibsFolder, "libpsmkdc_jni.so").getAbsolutePath();
|
||||
String libDefaultFile = new File(nativeLibsFolder, "libdefault.so").getAbsolutePath();
|
||||
String realLibDefaultFile = new File(nativeLibsFolder, "libdefault_real.so").getAbsolutePath();
|
||||
String folderContainingLibs = new File(nativeLibsFolder).getParentFile().getAbsolutePath();
|
||||
|
||||
// Check if the app is installed to the SD card or not.
|
||||
boolean installedToSd = !nativeLibsFolder.startsWith("/data");
|
||||
|
||||
// get the owner of the file ..
|
||||
String systemUid = getFileOwner(libPsmKdcFile);
|
||||
|
||||
if(installedToSd) systemUid = null; // /mnt/sec is FAT32, and doesnt support unix permissions.
|
||||
if(installedToSd) remountRw(folderContainingLibs); // the /mnt/asec folder is read only normally, so make ir read/write
|
||||
|
||||
if(!fileExistRoot(realLibDefaultFile)) {
|
||||
// if libdefault_real.so not found, then rename libdefault.so to libdefault_real.so ...
|
||||
moveFileRoot(libDefaultFile, realLibDefaultFile);
|
||||
|
||||
// if were on android 5.x or later, we need to patch libdefault.
|
||||
if(Build.VERSION.SDK_INT >= 21){
|
||||
String cachedLibDefault = new File(ctx.getCacheDir(), "libdefault_real.so").toString();
|
||||
|
||||
Root.copyChmodAndChown(realLibDefaultFile, cachedLibDefault, 777, String.valueOf(Helper.getMyUid(ctx)));
|
||||
patchLibdefault(cachedLibDefault);
|
||||
Root.copyChmodAndChown(cachedLibDefault, realLibDefaultFile, 755, systemUid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// unpack the library files ...
|
||||
unpackResourceToLocationRoot(R.raw.libdefault, libDefaultFile, 755, "system");
|
||||
unpackResourceToLocationRoot(R.raw.libpsmkdc_jni, libPsmKdcFile, 755, "system");
|
||||
unpackResourceToLocationRoot(ctx, R.raw.libdefault, libDefaultFile, 755, systemUid);
|
||||
unpackResourceToLocationRoot(ctx, R.raw.libpsmkdc_jni, libPsmKdcFile, 755, systemUid);
|
||||
|
||||
if(installedToSd) remountRo(folderContainingLibs);
|
||||
|
||||
|
||||
}
|
||||
private void installDatabase() throws Exception {
|
||||
String databasesFolder = new File(getPsmApp().dataDir, "databases").getAbsolutePath();
|
||||
String databasesFolder = new File(getPsmAppInfo(ctx).dataDir, "databases").getAbsolutePath();
|
||||
String libraryDbFile = new File(databasesFolder, "library.db").getAbsolutePath();
|
||||
|
||||
unpackResourceToLocationRoot(R.raw.library, libraryDbFile, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
unpackResourceToLocationRoot(ctx, R.raw.library, libraryDbFile, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
|
||||
private void installDeviceList() throws PackageManager.NameNotFoundException, IOException, Shell.ShellDiedException {
|
||||
String cacheFolder = new File(getPsmApp().dataDir, "cache").getAbsolutePath();
|
||||
String cacheFolder = new File(getPsmAppInfo(ctx).dataDir, "cache").getAbsolutePath();
|
||||
|
||||
// extract the regular device list ...
|
||||
String deviceList2File = new File(cacheFolder, "deviceList2.dat").getAbsolutePath();
|
||||
unpackResourceToLocationRoot(R.raw.device_list2, deviceList2File, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
unpackResourceToLocationRoot(ctx, R.raw.device_list2, deviceList2File, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
|
||||
|
||||
// extract the additional certified devices list ...
|
||||
String additionalCertsFile = new File(cacheFolder, "addtionalCertifiedDevList.dat").getAbsolutePath();
|
||||
unpackResourceToLocationRoot(R.raw.aditional_certified_devices_list, additionalCertsFile, 660, String.valueOf(stringEncryptor.getPsmUid()));
|
||||
unpackResourceToLocationRoot(ctx, R.raw.aditional_certified_devices_list, additionalCertsFile, 660, String.valueOf(Helper.getPsmUid(ctx)));
|
||||
}
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
Shell.setRedirectDeprecated(false);
|
||||
|
||||
dialog = new ProgressDialog(ctx);
|
||||
dialog.setTitle("Installing NoPsmDrm ...");
|
||||
if(!Helper.isNoPsmDrmInstalled(ctx))
|
||||
dialog.setTitle("Installing NoPsmDrm ...");
|
||||
else
|
||||
dialog.setTitle("Updating NoPsmDrm ...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setCancelable(false);
|
||||
|
@ -223,10 +388,9 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
protected Void doInBackground(Void... voids) {
|
||||
|
||||
try {
|
||||
Root.setContext(ctx);
|
||||
killApplication("com.playstation.psstore");
|
||||
Helper.killPsm(ctx);
|
||||
|
||||
if (isPsmInstalled()) {
|
||||
if (isPsmInstalled(ctx)) {
|
||||
backupPsm();
|
||||
makeDirs();
|
||||
installSharedPrefs();
|
||||
|
@ -240,7 +404,7 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
}
|
||||
catch(Exception e){
|
||||
this.wasError = true;
|
||||
this.errorMsg = e.getMessage();
|
||||
this.errorMsg = e.toString();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -250,7 +414,9 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
dialog.dismiss();
|
||||
handler.post(() -> {
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
if(wasError) {
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
|
@ -260,11 +426,16 @@ public class NoPsmDrmInstaller extends AsyncTask<Void, Void, Void> {
|
|||
.setPositiveButton("Ok",null).show();
|
||||
}
|
||||
else{
|
||||
handler.post(() ->{
|
||||
Startup.setPsmInstalled((Activity)ctx);
|
||||
});
|
||||
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Installed!")
|
||||
.setTitle("Finished!")
|
||||
.setMessage("Your PSM Application was patched successfully!\n\nNote: WI-FI has to be turned off for games to work.")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", null).show();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Permissions {
|
||||
public static boolean checkHavePermissions(Activity ctx, String[] permissions) {
|
||||
for(String permission : permissions){
|
||||
if (ActivityCompat.checkSelfPermission(ctx, permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public static void requestPermissionsIfNeeded(Activity ctx, String[] permissions) {
|
||||
ArrayList<String> needPerms = new ArrayList<String>();
|
||||
for(String perm : permissions){
|
||||
if(ActivityCompat.checkSelfPermission(ctx, perm) != PackageManager.PERMISSION_GRANTED){
|
||||
needPerms.add(perm);
|
||||
}
|
||||
}
|
||||
if(!needPerms.isEmpty())
|
||||
ActivityCompat.requestPermissions(ctx, needPerms.toArray(new String[0]), 1);
|
||||
}
|
||||
public static boolean checkPermsAlert(Activity ctx){
|
||||
if(Build.VERSION.SDK_INT >= 23){
|
||||
if (!Permissions.checkHavePermissions(ctx, new String[] {
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.KILL_BACKGROUND_PROCESSES,
|
||||
Manifest.permission.CHANGE_WIFI_STATE,
|
||||
Manifest.permission.ACCESS_WIFI_STATE,
|
||||
Manifest.permission.INTERNET,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
})) {
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setTitle("Missing permissions")
|
||||
.setMessage("Missing required permissions, please grant all requested permissions\nPHONE is needed only to read IMEI for deriving \"ConsoleID\"\nSTORAGE is needed for backing up existing PSM data")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
Permissions.requestPermissions(ctx);
|
||||
}
|
||||
}).show();
|
||||
|
||||
return false;
|
||||
}
|
||||
else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void requestPermissions(Activity ctx) {
|
||||
if(Build.VERSION.SDK_INT >= 23){
|
||||
requestPermissionsIfNeeded(ctx, new String[] {
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||
Manifest.permission.KILL_BACKGROUND_PROCESSES,
|
||||
Manifest.permission.CHANGE_WIFI_STATE,
|
||||
Manifest.permission.ACCESS_WIFI_STATE,
|
||||
Manifest.permission.INTERNET,
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import eu.chainfire.libsuperuser.Shell;
|
||||
|
||||
public class Root {
|
||||
private static String busyboxBinary = null;
|
||||
private static Context ctx = null;
|
||||
|
||||
public static void killApplication(String processName) throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Killing process: " + processName);
|
||||
Shell.Pool.SU.run(new String[] { busyboxBinary + " pkill '" + processName +"'" });
|
||||
}
|
||||
public static boolean fileExistRoot(String filename) throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "FileExistRoot: " + filename);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " stat '" + filename +"'" });
|
||||
return res == 0;
|
||||
}
|
||||
|
||||
public static void tarRoot(String src, String dst) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "TarRoot: " + src + " : "+ dst);
|
||||
int res = Shell.Pool.SU.run(new String[]{busyboxBinary + " tar c '" + src + "' -f '" + dst +"'"});
|
||||
if (res != 0) {
|
||||
throw new IOException("Failed to tar " + src);
|
||||
}
|
||||
}
|
||||
public static void remountRo(String foldername) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Remounting as RO: " + foldername);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mount -o ro,remount '"+foldername+"'"}
|
||||
);
|
||||
if(res != 0){
|
||||
throw new IOException("Failed to remmount as RO: "+foldername);
|
||||
}
|
||||
}
|
||||
public static void remountRw(String foldername) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Remounting as RW: " + foldername);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mount -o rw,remount '"+foldername+"'"}
|
||||
);
|
||||
if(res != 0){
|
||||
throw new IOException("Failed to remmount as RW: "+foldername);
|
||||
}
|
||||
}
|
||||
public static void moveFileRoot(String filename, String destFilename) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "MoveRoot: " + filename + " : "+ destFilename);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mv '"+ filename + "' '" +destFilename +"'"});
|
||||
if(res != 0){
|
||||
throw new IOException("Failed to rename "+filename+" to "+ destFilename);
|
||||
}
|
||||
}
|
||||
public static void mkdirAndChmodChown(String directory, int chmod, String chown) throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "MkdirAndChown: " + directory);
|
||||
Shell.Pool.SU.run(new String[]{
|
||||
busyboxBinary + " mkdir '" + directory +"'",
|
||||
busyboxBinary + " chmod " + String.valueOf(chmod) +" '"+directory+"'",
|
||||
busyboxBinary + " chown " + chown +":"+chown+" '"+directory+"'"
|
||||
});
|
||||
}
|
||||
public static void copyChmodAndChown(String srcFile, String dstFile, int chmod, String chown) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "CopyAndChown: " + srcFile +" : "+dstFile);
|
||||
int res = Shell.Pool.SU.run(new String[]{
|
||||
busyboxBinary + " cp '" + srcFile +"' '"+dstFile+"'",
|
||||
busyboxBinary + " chmod " + String.valueOf(chmod) +" '"+dstFile+"'",
|
||||
busyboxBinary + " chown " + chown +":"+chown+" '"+dstFile+"'"
|
||||
});
|
||||
|
||||
if(res != 0){
|
||||
throw new IOException("Failed to copy & change mode.");
|
||||
}
|
||||
}
|
||||
private static void copyTo(InputStream inpStream, OutputStream outStream) throws IOException {
|
||||
int totalRead = 0;
|
||||
byte[] buffer = new byte[0x1000];
|
||||
|
||||
do {
|
||||
totalRead = inpStream.read(buffer, 0, buffer.length);
|
||||
outStream.write(buffer, 0, totalRead);
|
||||
}
|
||||
while(totalRead >= buffer.length);
|
||||
|
||||
outStream.flush();
|
||||
}
|
||||
public static void unpackResourceToLocationRoot(int resourceId, String outputDir, int chmod, String chown) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Unpacking resource and root copying to : " + outputDir);
|
||||
File tmpFile = File.createTempFile("tmp", "file", ctx.getCacheDir());
|
||||
tmpFile.createNewFile();
|
||||
|
||||
unpackResource(resourceId, tmpFile);
|
||||
copyChmodAndChown(tmpFile.getAbsolutePath(), outputDir, chmod, chown);
|
||||
|
||||
tmpFile.delete();
|
||||
}
|
||||
private static void unpackResource(int resourceId, File outputFile) throws IOException {
|
||||
Log.i("ROOT", "Unpacking resource to : " + outputFile);
|
||||
InputStream resourceStream = ctx.getResources().openRawResource(resourceId);
|
||||
|
||||
FileOutputStream fs = new FileOutputStream(outputFile, false);
|
||||
copyTo(resourceStream, fs);
|
||||
fs.close();
|
||||
|
||||
resourceStream.close();
|
||||
}
|
||||
public static void writeTxtFile(String txtFile, String txt) throws IOException {
|
||||
Log.i("ROOT", "Writing: " + txtFile);
|
||||
FileWriter txtStream = new FileWriter(txtFile);
|
||||
txtStream.write(txt);
|
||||
txtStream.close();
|
||||
}
|
||||
private static void setupBusyBox() throws IOException {
|
||||
Log.i("ROOT","Creating busybox binary");
|
||||
File tmpFile = new File(ctx.getCacheDir(), "busybox");
|
||||
if(!tmpFile.exists()){
|
||||
tmpFile.createNewFile();
|
||||
|
||||
if(tmpFile.setExecutable(true,false)) {
|
||||
unpackResource(R.raw.busybox, tmpFile);
|
||||
}
|
||||
else {
|
||||
throw new IOException("failed to extract busybox binary.");
|
||||
}
|
||||
}
|
||||
|
||||
busyboxBinary = tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static void setContext(Context context) throws IOException {
|
||||
ctx = context;
|
||||
setupBusyBox();
|
||||
}
|
||||
}
|
|
@ -1,22 +1,49 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import eu.chainfire.libsuperuser.Shell;
|
||||
import static com.psmreborn.nopsmdrm.Helper.*;
|
||||
import com.psmreborn.pscertified.PlayStationCertified;
|
||||
import com.psmreborn.shared.Helper;
|
||||
import com.psmreborn.shared.Root;
|
||||
|
||||
public class Startup extends AsyncTask<Void, Void, Void> {
|
||||
private Context ctx;
|
||||
|
||||
private Activity ctx = null;
|
||||
private Handler handler = null;
|
||||
private boolean wasError = false;
|
||||
private String errorMsg = "";
|
||||
|
||||
public Startup(Context context) {
|
||||
|
||||
public static void updateNames(Activity activity) {
|
||||
Button installButton = (Button) activity.findViewById(R.id.installPsm);
|
||||
|
||||
if(!PlayStationCertified.isPlaystationCertified(activity.getApplicationContext()))
|
||||
installButton.setText("Install PS Certify");
|
||||
else if(!Helper.isPsmInstalled(activity))
|
||||
installButton.setText("Install PSM");
|
||||
else if(Helper.isNoPsmDrmInstalled(activity))
|
||||
installButton.setText("Update NoPsmDrm");
|
||||
else
|
||||
installButton.setText("Install NoPsmDrm");
|
||||
}
|
||||
public static void setPsmInstalled(Activity activity){
|
||||
Button backupGamesButton = (Button) activity.findViewById(R.id.dumpGames);
|
||||
Button takeOwnershipButton = (Button) activity.findViewById(R.id.takeOwnership);
|
||||
|
||||
backupGamesButton.setEnabled(true);
|
||||
if(Build.VERSION.SDK_INT >= 30)
|
||||
takeOwnershipButton.setEnabled(true);
|
||||
|
||||
updateNames(activity);
|
||||
}
|
||||
|
||||
public Startup(Activity context) {
|
||||
this.handler = new Handler(context.getMainLooper());
|
||||
this.ctx = context;
|
||||
}
|
||||
|
||||
|
@ -24,22 +51,26 @@ public class Startup extends AsyncTask<Void, Void, Void> {
|
|||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
|
||||
if(!Shell.SU.available()){
|
||||
if(!Root.init(ctx)){
|
||||
wasError = true;
|
||||
errorMsg = "Unable to get root permission.";
|
||||
return null;
|
||||
}
|
||||
if(!Helper.isPsmInstalled()){
|
||||
if(Helper.isPsmInstalled(ctx) && Helper.getPsmAppPkg(ctx).versionCode != 1170) {
|
||||
wasError = true;
|
||||
errorMsg = "PSM Application is not installed, please install it first!";
|
||||
}
|
||||
if(!(getPsmApp().sourceDir.startsWith("/data/") || getPsmApp().sourceDir.startsWith("/system/"))){
|
||||
wasError = true;
|
||||
errorMsg = "PSM Application is installed to the SD Card not internal storage.";
|
||||
errorMsg = "PSM Application is installed, but an older version, please update to 1.7.0 (have: "+String.valueOf(Helper.getPsmAppPkg(ctx).versionCode)+")";
|
||||
return null;
|
||||
}
|
||||
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
|
||||
wasError = true;
|
||||
errorMsg = "No SD Card inserted.";
|
||||
return null;
|
||||
}
|
||||
|
||||
Permissions.requestPermissions(this.ctx);
|
||||
Helper.killPsm(ctx);
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
wasError = true;
|
||||
errorMsg = e.getMessage();
|
||||
|
@ -49,13 +80,26 @@ public class Startup extends AsyncTask<Void, Void, Void> {
|
|||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
TextView statusTV = (TextView) ((Activity)ctx).findViewById(R.id.errorMsg);
|
||||
Button installButton = (Button) ((Activity)ctx).findViewById(R.id.installPsm);
|
||||
|
||||
|
||||
if(!wasError) {
|
||||
Button installButton = (Button) ((Activity)ctx).findViewById(R.id.installPsm);
|
||||
installButton.setEnabled(true);
|
||||
handler.post(() -> {
|
||||
statusTV.setText("");
|
||||
|
||||
if(Helper.isNoPsmDrmInstalled(ctx)){
|
||||
setPsmInstalled(ctx);
|
||||
};
|
||||
updateNames(ctx);
|
||||
|
||||
installButton.setEnabled(true);
|
||||
});
|
||||
}
|
||||
else{
|
||||
TextView rootDetectedTV = (TextView) ((Activity)ctx).findViewById(R.id.errorMsg);
|
||||
rootDetectedTV.setText("Error: "+errorMsg);
|
||||
handler.post(() -> {
|
||||
statusTV.setText("Error: " + errorMsg);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.psmreborn.nopsmdrm.pscertified;
|
||||
package com.psmreborn.pscertified;
|
||||
|
||||
|
||||
public class CertifiedDeviceEntry {
|
|
@ -1,4 +1,4 @@
|
|||
package com.psmreborn.nopsmdrm.pscertified;
|
||||
package com.psmreborn.pscertified;
|
||||
|
||||
public class CertifiedDeviceList {
|
||||
private String manufacturer = null;
|
|
@ -1,4 +1,4 @@
|
|||
package com.psmreborn.nopsmdrm.pscertified;
|
||||
package com.psmreborn.pscertified;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
@ -18,12 +18,7 @@ public class PlayStationCertified {
|
|||
new CertifiedDeviceList("FUJITSU MOBILE COMMUNICATIONS LIMITED", new CertifiedDeviceEntry[] { new CertifiedDeviceEntry("F05E", 15) }),
|
||||
};
|
||||
|
||||
private Context ctx;
|
||||
public PlayStationCertified(Context context){
|
||||
ctx = context;
|
||||
}
|
||||
|
||||
private boolean isInDeviceList() {
|
||||
private static boolean isInDeviceList() {
|
||||
String brand = Build.BRAND;
|
||||
String manufacturer = Build.MANUFACTURER;
|
||||
int osVer = Build.VERSION.SDK_INT;
|
||||
|
@ -38,7 +33,7 @@ public class PlayStationCertified {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isPlaystationCertified() {
|
||||
public static boolean isPlaystationCertified(Context ctx) {
|
||||
HashSet installedFrameworks = new HashSet(Arrays.asList(ctx.getPackageManager().getSystemSharedLibraryNames()));
|
||||
if(installedFrameworks.contains("com.playstation.playstationcertified")){
|
||||
return true;
|
|
@ -1,11 +1,10 @@
|
|||
package com.psmreborn.nopsmdrm.pscertified;
|
||||
package com.psmreborn.pscertified;
|
||||
|
||||
import static com.psmreborn.nopsmdrm.Root.*;
|
||||
import static com.psmreborn.shared.Root.*;
|
||||
|
||||
import com.psmreborn.nopsmdrm.MainActivity;
|
||||
import com.psmreborn.nopsmdrm.NoPsmDrmInstaller;
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
import com.psmreborn.nopsmdrm.Root;
|
||||
import com.psmreborn.shared.Files;
|
||||
import com.psmreborn.shared.Root;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
|
@ -13,7 +12,8 @@ import android.app.ProgressDialog;
|
|||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.PowerManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
|
@ -25,8 +25,13 @@ public class PsCertificatesInstaller extends AsyncTask<Void, Void, Void> {
|
|||
private ProgressDialog dialog = null;
|
||||
private boolean wasError = false;
|
||||
private String errorMsg = "";
|
||||
Context ctx = null;
|
||||
private Context ctx = null;
|
||||
|
||||
public void installPlaystationCertificationMagisk() throws Exception {
|
||||
File magiskZip = new File(ctx.getCacheDir(), "MagiskCertify.zip");
|
||||
Files.unpackResource(ctx, R.raw.magiskcertify, magiskZip);
|
||||
Root.installMagiskModule(magiskZip.getAbsolutePath());
|
||||
}
|
||||
public void installPlaystationCertification() throws IOException, Shell.ShellDiedException {
|
||||
// remount /system as read-write
|
||||
remountRw("/system");
|
||||
|
@ -34,8 +39,8 @@ public class PsCertificatesInstaller extends AsyncTask<Void, Void, Void> {
|
|||
String psCertifiedPermissionFile = "/system/etc/permissions/com.playstation.playstationcertified.xml";
|
||||
String psCertifiedJarFile = "/system/framework/com.playstation.playstationcertified.jar";
|
||||
|
||||
unpackResourceToLocationRoot(R.raw.ps_certified_permission, psCertifiedPermissionFile, 644, "root");
|
||||
unpackResourceToLocationRoot(R.raw.ps_certified_jar, psCertifiedJarFile, 644, "root");
|
||||
Root.unpackResourceToLocationRoot(ctx, R.raw.ps_certified_permission, psCertifiedPermissionFile, 644, "0");
|
||||
Root.unpackResourceToLocationRoot(ctx, R.raw.ps_certified_jar, psCertifiedJarFile, 644, "0");
|
||||
|
||||
// make it read-only again.
|
||||
remountRo("/system");
|
||||
|
@ -47,8 +52,6 @@ public class PsCertificatesInstaller extends AsyncTask<Void, Void, Void> {
|
|||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
Shell.setRedirectDeprecated(false);
|
||||
|
||||
dialog = new ProgressDialog(ctx);
|
||||
dialog.setTitle("Installing PlayStation Certificates ...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
|
@ -59,8 +62,12 @@ public class PsCertificatesInstaller extends AsyncTask<Void, Void, Void> {
|
|||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
try {
|
||||
Root.setContext(ctx);
|
||||
installPlaystationCertification();
|
||||
if(Build.VERSION.SDK_INT >= 23) {
|
||||
installPlaystationCertificationMagisk();
|
||||
}
|
||||
else{
|
||||
installPlaystationCertification();
|
||||
}
|
||||
}
|
||||
catch(Exception e){
|
||||
this.wasError = true;
|
||||
|
@ -84,12 +91,17 @@ public class PsCertificatesInstaller extends AsyncTask<Void, Void, Void> {
|
|||
else{
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Installed!")
|
||||
.setMessage("Your device is now \"Playstation Certified\"\n(You may have to reboot for changes to take effect)\n\nWould you like to patch the PSM application also?")
|
||||
.setMessage("Your device is now \"Playstation Certified\"\n(You have to reboot for changes to take effect)\nDo you want to reboot?")
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
(new NoPsmDrmInstaller(PsCertificatesInstaller.this.ctx)).execute();
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Handler handler = new Handler(PsCertificatesInstaller.this.ctx.getMainLooper());
|
||||
handler.post(() -> {
|
||||
try {
|
||||
Root.reboot();
|
||||
} catch (Shell.ShellDiedException e) {}
|
||||
});
|
||||
}
|
||||
})
|
||||
.setNegativeButton("No", null).show();
|
|
@ -0,0 +1,135 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.os.Handler;
|
||||
|
||||
import com.psmreborn.nopsmdrm.Startup;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class Downloader {
|
||||
static boolean wasError = false;
|
||||
static String errorMsg = "";
|
||||
|
||||
private static void showErrorMessage(Activity ctx){
|
||||
new AlertDialog.Builder(ctx)
|
||||
.setTitle("Error Occurred.")
|
||||
.setMessage(errorMsg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok",null).show();
|
||||
}
|
||||
private static void showInstallSuccessMsg(Activity ctx, String filename){
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Install complete!")
|
||||
.setMessage("Sucessfully installed: "+ filename)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok", null).show();
|
||||
}
|
||||
private static void showDownloadSuccessMsg(Activity ctx, String filename){
|
||||
new AlertDialog.Builder((Activity)ctx)
|
||||
.setTitle("Download complete!")
|
||||
.setMessage("Sucessfully downloaded: "+ filename)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton("Ok",null).show();
|
||||
}
|
||||
|
||||
|
||||
private static void download(String uri, String dstFile) throws IOException {
|
||||
|
||||
URL url = new URL(uri);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.connect();
|
||||
|
||||
InputStream input = new BufferedInputStream(url.openStream(), 8192);
|
||||
OutputStream output = new FileOutputStream(dstFile);
|
||||
|
||||
Files.copyTo(input, output);
|
||||
output.close();
|
||||
input.close();
|
||||
}
|
||||
public static void downloadAndInstall(Activity ctx, String url){
|
||||
ProgressDialog dialog = new ProgressDialog(ctx);
|
||||
final String filename = new File(url).getName();
|
||||
|
||||
final String saveLocation = new File(ctx.getCacheDir(), filename).getAbsolutePath();
|
||||
|
||||
dialog.setTitle("Downloading "+filename+"...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setCancelable(false);
|
||||
dialog.show();
|
||||
|
||||
new Thread(() -> {
|
||||
Handler handler = new Handler(ctx.getMainLooper());
|
||||
|
||||
Helper.killPsm(ctx);
|
||||
try {
|
||||
download(url, saveLocation);
|
||||
handler.post(() -> {dialog.setTitle("Installing "+filename+"...");});
|
||||
Root.installRoot(saveLocation);
|
||||
} catch (Exception e) {
|
||||
wasError = true;
|
||||
errorMsg = e.toString();
|
||||
}
|
||||
|
||||
handler.post(() -> {
|
||||
dialog.dismiss();
|
||||
Startup.updateNames(ctx);
|
||||
|
||||
if(wasError){
|
||||
showErrorMessage(ctx);
|
||||
}
|
||||
else {
|
||||
showInstallSuccessMsg(ctx, filename);
|
||||
}
|
||||
});
|
||||
|
||||
}).start();
|
||||
}
|
||||
public static void downloadFile(Activity ctx, String url, String saveLocation) {
|
||||
ProgressDialog dialog = new ProgressDialog(ctx);
|
||||
final String filename = new File(url).getName();
|
||||
|
||||
dialog.setTitle("Downloading "+filename+"...");
|
||||
dialog.setMessage("Please Wait ...");
|
||||
dialog.setIndeterminate(true);
|
||||
dialog.setCancelable(false);
|
||||
dialog.show();
|
||||
|
||||
new Thread(() -> {
|
||||
Handler handler = new Handler(ctx.getMainLooper());
|
||||
|
||||
Helper.killPsm(ctx);
|
||||
try {
|
||||
download(url, saveLocation);
|
||||
} catch (IOException e) {
|
||||
wasError = true;
|
||||
errorMsg = e.toString();
|
||||
}
|
||||
|
||||
handler.post(() -> {
|
||||
dialog.dismiss();
|
||||
Startup.updateNames(ctx);
|
||||
|
||||
if(wasError){
|
||||
showErrorMessage(ctx);
|
||||
}
|
||||
else {
|
||||
showDownloadSuccessMsg(ctx, filename);
|
||||
}
|
||||
});
|
||||
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class Files {
|
||||
public static void unpackResource(Context ctx, int resourceId, File outputFile) throws IOException {
|
||||
Log.i("ROOT", "Unpacking resource to : " + outputFile);
|
||||
InputStream resourceStream = ctx.getResources().openRawResource(resourceId);
|
||||
|
||||
FileOutputStream fs = new FileOutputStream(outputFile, false);
|
||||
copyTo(resourceStream, fs);
|
||||
fs.close();
|
||||
|
||||
resourceStream.close();
|
||||
}
|
||||
public static void writeTxtFile(String txtFile, String txt) throws IOException {
|
||||
Log.i("ROOT", "Writing: " + txtFile);
|
||||
FileWriter txtStream = new FileWriter(txtFile);
|
||||
txtStream.write(txt);
|
||||
txtStream.close();
|
||||
}
|
||||
|
||||
public static void copyTo(InputStream inpStream, OutputStream outStream) throws IOException {
|
||||
byte[] buffer = new byte[8192];
|
||||
int totalRead;
|
||||
|
||||
while(true){
|
||||
totalRead = inpStream.read(buffer, 0, buffer.length);
|
||||
if(totalRead == -1)
|
||||
break;
|
||||
outStream.write(buffer, 0, totalRead);
|
||||
}
|
||||
|
||||
outStream.flush();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Helper {
|
||||
|
||||
public static boolean isNoPsmDrmInstalled(Context ctx) {
|
||||
try {
|
||||
if(new File(getPsmAppInfo(ctx).nativeLibraryDir, "libdefault_real.so").exists()){
|
||||
return true;
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static long getDefaultAccountId(Context ctx){
|
||||
return Long.valueOf(ctx.getResources().getString(R.string.default_account_id), 16);
|
||||
}
|
||||
public static String getSharedPrefFromPsm(Context ctx, String prefName, String prefKey) throws Exception {
|
||||
|
||||
String psmSharedPrefs = new File(Helper.getPsmAppInfo(ctx).dataDir, "shared_prefs").getAbsolutePath();
|
||||
String psmPrefFile = new File(psmSharedPrefs, prefName+".xml").getAbsolutePath();
|
||||
|
||||
String mySharedPrefs = new File(Helper.getAppInfo(ctx, ctx.getPackageName()).dataDir, "shared_prefs").getAbsolutePath();
|
||||
String myPrefFile = new File(mySharedPrefs, prefName+".xml").getAbsolutePath();
|
||||
|
||||
if(!Root.fileExistRoot(psmSharedPrefs)) return null;
|
||||
|
||||
Root.mkdirAndChmodChown(mySharedPrefs, 771, String.valueOf(Helper.getMyUid(ctx)));
|
||||
Root.copyChmodAndChown(psmPrefFile, myPrefFile, 660, String.valueOf(Helper.getMyUid(ctx)));
|
||||
|
||||
SharedPreferences pref = ctx.getSharedPreferences(prefName, 0);
|
||||
return pref.getString(prefKey, null);
|
||||
}
|
||||
public static boolean isPsmInstalled(Context ctx){
|
||||
try {
|
||||
ctx.getPackageManager().getApplicationInfo(ctx.getResources().getString(R.string.psm_app_package_id), 0);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static int getMyUid(Context ctx) {
|
||||
try{
|
||||
ApplicationInfo myAppInfo = ctx.getPackageManager().getApplicationInfo(ctx.getPackageName(), 0);
|
||||
if(myAppInfo != null) {
|
||||
return myAppInfo.uid;
|
||||
}
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e) { };
|
||||
return 0;
|
||||
}
|
||||
public static int getPsmUid(Context ctx) {
|
||||
try{
|
||||
ApplicationInfo psmAppInfo = ctx.getPackageManager().getApplicationInfo(ctx.getResources().getString(R.string.psm_app_package_id), 0);
|
||||
if(psmAppInfo != null) {
|
||||
return psmAppInfo.uid;
|
||||
}
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e) { };
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static void killPsm(Context ctx){
|
||||
if(Build.VERSION.SDK_INT >= 34){
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Root.killProcess(ctx.getResources().getString(R.string.psm_app_package_id));
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
else{
|
||||
ActivityManager am = (ActivityManager) ctx.getSystemService(Activity.ACTIVITY_SERVICE);
|
||||
if (am != null) {
|
||||
am.killBackgroundProcesses(ctx.getResources().getString(R.string.psm_app_package_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAndroidIdOfPsm(Context ctx){
|
||||
return SsaidParser.getAndroidIdOfApp(ctx, ctx.getResources().getString(R.string.psm_app_package_id));
|
||||
}
|
||||
public static PackageInfo getPackageInfo(Context ctx, String pkg) throws PackageManager.NameNotFoundException {
|
||||
return ctx.getPackageManager().getPackageInfo(pkg, 0);
|
||||
}
|
||||
public static ApplicationInfo getAppInfo(Context ctx, String pkg) throws PackageManager.NameNotFoundException {
|
||||
return ctx.getPackageManager().getApplicationInfo(pkg, 0);
|
||||
}
|
||||
public static PackageInfo getPsmAppPkg(Context ctx) throws PackageManager.NameNotFoundException {
|
||||
return Helper.getPackageInfo(ctx,ctx.getResources().getString(R.string.psm_app_package_id));
|
||||
}
|
||||
public static ApplicationInfo getPsmAppInfo(Context ctx) throws PackageManager.NameNotFoundException {
|
||||
return Helper.getAppInfo(ctx,ctx.getResources().getString(R.string.psm_app_package_id));
|
||||
}
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
public static String getDateTime(){
|
||||
return new SimpleDateFormat("MM_dd_yyyy_HH_mm_ss").format(new Date());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class Logger {
|
||||
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
if(bytes == null) return "(nulL)";
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
public static void logText(String description, String text){
|
||||
Log.i("NoPsmDrmApp", description.toUpperCase() + " = "+text);
|
||||
}
|
||||
public static void logBytes(String description, byte[] data){
|
||||
Log.i("NoPsmDrmApp", description.toUpperCase() + " = " + bytesToHex(data));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,256 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.psmreborn.nopsmdrm.R;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.chainfire.libsuperuser.Shell;
|
||||
|
||||
public class Root {
|
||||
private static String busyboxBinary = null;
|
||||
|
||||
public static void killProcess(String processName) throws Shell.ShellDiedException {
|
||||
int res = 0;
|
||||
do{
|
||||
res = Shell.Pool.SU.run(new String[] { busyboxBinary + " pkill -9 '"+processName+"'" });
|
||||
} while(res == 0);
|
||||
}
|
||||
public static void installMagiskModule(String magiskModuleFile) throws Exception {
|
||||
int res = 0;
|
||||
res = Shell.Pool.SU.run(new String[] { "magisk --install-module '"+magiskModuleFile+"'"});
|
||||
if(res != 0){
|
||||
throw new Exception("Magisk module failed to install.");
|
||||
}
|
||||
}
|
||||
public static void installRoot(String apkFile) throws Exception {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
int res = 0;
|
||||
|
||||
String apkFileName = new File("/data/local/tmp", new File(apkFile).getName()).getAbsolutePath();
|
||||
moveChmodAndChown(apkFile, apkFileName, 777, null);
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 34){
|
||||
res = Shell.Pool.SU.run(new String[] { "pm install --bypass-low-target-sdk-block '"+apkFileName+"'"}, results, null, true);
|
||||
}
|
||||
else{
|
||||
res = Shell.Pool.SU.run(new String[] { "pm install '"+apkFileName+"'" },results, null, true);
|
||||
}
|
||||
|
||||
// delete apk now its installed.
|
||||
deleteRoot(apkFileName);
|
||||
|
||||
if(res != 0) {
|
||||
throw new Exception(results.get(0));
|
||||
}
|
||||
}
|
||||
public static void deleteRoot(String filename) throws Shell.ShellDiedException, IOException {
|
||||
Log.i("ROOT", "deleting file: "+filename);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " rm -f '"+filename+"'" });
|
||||
if(res != 0){
|
||||
throw new IOException("failed to remove file.");
|
||||
}
|
||||
}
|
||||
public static void reboot() throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Rebooting");
|
||||
Shell.Pool.SU.run(new String[] { "reboot" });
|
||||
}
|
||||
public static boolean fileExistRoot(String filename) throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "FileExistRoot: " + filename);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " stat '" + filename +"'" });
|
||||
return res == 0;
|
||||
}
|
||||
|
||||
public static String[] findFiles(String filePath, String fileName) throws Shell.ShellDiedException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " find '" + filePath + "' -name '"+fileName+"'" }, results, null, true);
|
||||
if(res == 0) {
|
||||
String[] foundFiles = new String[results.size()];
|
||||
for(int i = 0; i < results.size(); i++)
|
||||
foundFiles[i] = results.get(i);
|
||||
|
||||
return foundFiles;
|
||||
}
|
||||
return new String[]{};
|
||||
}
|
||||
|
||||
public static void chownRoot(String filePath, String owner, String group) throws Shell.ShellDiedException, IOException {
|
||||
Log.i("ROOT", "ChownRoot: " + filePath);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " chown "+owner+":"+group+" -R '" + filePath +"'" });
|
||||
if(res != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("Failed to chown: "+filePath + " ("+String.valueOf(res)+")");
|
||||
}
|
||||
}
|
||||
public static String getFileGroup(String filePath) throws Shell.ShellDiedException, IOException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " stat '" + filePath + "' -c %g" }, results, null, true);
|
||||
if(res == 0) {
|
||||
return results.get(0);
|
||||
}
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("failed to get group: "+filePath);
|
||||
}
|
||||
public static String getFileOwner(String filePath) throws Shell.ShellDiedException, IOException {
|
||||
ArrayList<String> results = new ArrayList<String>();
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary + " stat '" + filePath + "' -c %u" }, results, null, true);
|
||||
if(res == 0) {
|
||||
return results.get(0);
|
||||
}
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("failed to get owner: "+filePath);
|
||||
}
|
||||
|
||||
public static void tarRoot(ArrayList<String> src, String dst) throws IOException, Shell.ShellDiedException {
|
||||
if(src.size() <= 0){
|
||||
throw new IOException("tar src was empty.");
|
||||
};
|
||||
|
||||
Log.i("ROOT", "TarRoot: "+ dst);
|
||||
|
||||
StringBuilder tarFiles = new StringBuilder();
|
||||
for(String file : src) {
|
||||
tarFiles.append("'");
|
||||
tarFiles.append(file);
|
||||
tarFiles.append("' ");
|
||||
}
|
||||
|
||||
int res = Shell.Pool.SU.run(new String[]{busyboxBinary + " tar -cf '" + dst + "' "+tarFiles.toString()});
|
||||
if (res != 0) {
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("Failed to tar " + dst);
|
||||
}
|
||||
}
|
||||
public static void remountRo(String foldername) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Remounting as RO: " + foldername);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mount -o ro,remount '"+foldername+"'"});
|
||||
if(res != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("Failed to remmount: "+foldername + " ("+String.valueOf(res)+")");
|
||||
}
|
||||
}
|
||||
public static void remountRw(String foldername) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Remounting as RW: " + foldername);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mount -o rw,remount '"+foldername+"'"});
|
||||
if(res != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("Failed to remmount: "+foldername + " ("+String.valueOf(res)+")");
|
||||
}
|
||||
}
|
||||
public static void moveFileRoot(String filename, String destFilename) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "MoveRoot: " + filename + " : "+ destFilename);
|
||||
int res = Shell.Pool.SU.run(new String[] { busyboxBinary +" mv '"+ filename + "' '" +destFilename +"'"});
|
||||
if(res != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(res) +")");
|
||||
throw new IOException("Failed to rename "+filename+" to "+ destFilename);
|
||||
}
|
||||
}
|
||||
public static void mkdirAndChmodChown(String directory, int chmod, String chown) throws Shell.ShellDiedException {
|
||||
Log.i("ROOT", "MkdirAndChown: " + directory);
|
||||
Shell.Pool.SU.run(new String[]{
|
||||
busyboxBinary + " mkdir '" + directory +"'",
|
||||
busyboxBinary + " chmod " + String.valueOf(chmod) +" '"+directory+"'",
|
||||
busyboxBinary + " chown " + chown +":"+chown+" '"+directory+"'"
|
||||
});
|
||||
}
|
||||
|
||||
public static void moveChmodAndChown(String srcFile, String dstFile, int chmod, String chown) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "MoveAndChown: " + srcFile +" : "+dstFile);
|
||||
|
||||
|
||||
int chownRes = 0;
|
||||
int mvRes = 0;
|
||||
int chmodRes = 0;
|
||||
|
||||
mvRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " mv '" + srcFile +"' '"+dstFile+"'"});
|
||||
if(chmod != 0)
|
||||
chmodRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " chmod " + String.valueOf(chmod) +" '"+dstFile+"'"});
|
||||
if(chown != null)
|
||||
chownRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " chown " + chown +":"+chown+" '"+dstFile+"'"});
|
||||
|
||||
if(mvRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(mvRes) +")");
|
||||
throw new IOException("Failed to move file. " + String.valueOf(mvRes));
|
||||
}
|
||||
if(chmodRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(chmodRes) +")");
|
||||
throw new IOException("Failed to chmod file. " + String.valueOf(chmodRes));
|
||||
}
|
||||
if(chownRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(chownRes) +")");
|
||||
throw new IOException("Failed to chown file. " + String.valueOf(chownRes));
|
||||
}
|
||||
|
||||
}
|
||||
public static void copyChmodAndChown(String srcFile, String dstFile, int chmod, String chown) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "CopyAndChown: " + srcFile +" : "+dstFile);
|
||||
|
||||
|
||||
int chownRes = 0;
|
||||
int cpRes = 0;
|
||||
int chmodRes = 0;
|
||||
|
||||
cpRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " cp '" + srcFile +"' '"+dstFile+"'"});
|
||||
if(chmod != 0)
|
||||
chmodRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " chmod " + String.valueOf(chmod) +" '"+dstFile+"'"});
|
||||
if(chown != null)
|
||||
chownRes = Shell.Pool.SU.run(new String[]{busyboxBinary + " chown " + chown +":"+chown+" '"+dstFile+"'"});
|
||||
|
||||
if(cpRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(cpRes) +")");
|
||||
throw new IOException("Failed to move file. " + String.valueOf(cpRes));
|
||||
}
|
||||
if(chmodRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(chmodRes) +")");
|
||||
throw new IOException("Failed to chmod file. " + String.valueOf(chmodRes));
|
||||
}
|
||||
if(chownRes != 0){
|
||||
Log.e("ROOT", "error (" + String.valueOf(chownRes) +")");
|
||||
throw new IOException("Failed to chown file. " + String.valueOf(chownRes));
|
||||
}
|
||||
|
||||
}
|
||||
public static void unpackResourceToLocationRoot(Context ctx, int resourceId, String outputDir, int chmod, String chown) throws IOException, Shell.ShellDiedException {
|
||||
Log.i("ROOT", "Unpacking resource and root copying to : " + outputDir);
|
||||
File tmpFile = File.createTempFile("tmp", "file", ctx.getCacheDir());
|
||||
tmpFile.createNewFile();
|
||||
|
||||
Files.unpackResource(ctx, resourceId, tmpFile);
|
||||
moveChmodAndChown(tmpFile.getAbsolutePath(), outputDir, chmod, chown);
|
||||
}
|
||||
|
||||
private static void setupBusyBox(Context ctx) throws IOException {
|
||||
Log.i("ROOT","Creating busybox binary");
|
||||
File tmpFile = new File(ctx.getCacheDir(), "busybox");
|
||||
if(!tmpFile.exists()) {
|
||||
tmpFile.createNewFile();
|
||||
|
||||
if(tmpFile.setExecutable(true,false)) {
|
||||
Files.unpackResource(ctx, R.raw.busybox, tmpFile);
|
||||
}
|
||||
else {
|
||||
throw new IOException("failed to extract busybox binary.");
|
||||
}
|
||||
}
|
||||
|
||||
busyboxBinary = tmpFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
public static boolean init(Context ctx) throws IOException {
|
||||
if(Shell.SU.available()){
|
||||
setupBusyBox(ctx);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package com.psmreborn.shared;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.provider.Settings;
|
||||
|
||||
import com.rosstonovsky.abxutils.BinaryXmlPullParser;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class SsaidParser {
|
||||
private static String parseSsaidText(FileInputStream stream, String searchPackage) throws XmlPullParserException, IOException {
|
||||
// parse xml file ...
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(false);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
xpp.setInput(stream, "UTF-8");
|
||||
int eventType = xpp.getEventType();
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if(eventType == XmlPullParser.START_TAG) {
|
||||
if(xpp.getName().equalsIgnoreCase("setting")){
|
||||
String packageName = xpp.getAttributeValue(null,"package");
|
||||
if(packageName != null && packageName.equalsIgnoreCase(searchPackage)){
|
||||
return xpp.getAttributeValue(null,"value");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private static String parseSsaidBinary(FileInputStream stream, String searchPackage) throws XmlPullParserException, IOException {
|
||||
// parse binary xml file ...
|
||||
BinaryXmlPullParser xpp = new BinaryXmlPullParser();
|
||||
xpp.setInput(stream, "UTF-8");
|
||||
int eventType = xpp.getEventType();
|
||||
while (eventType != BinaryXmlPullParser.END_DOCUMENT) {
|
||||
if(eventType == BinaryXmlPullParser.START_TAG) {
|
||||
if(xpp.getName().equalsIgnoreCase("setting")){
|
||||
String packageName = xpp.getAttributeValue("package");
|
||||
if(packageName != null && packageName.equalsIgnoreCase(searchPackage)){
|
||||
return xpp.getAttributeValue("value");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String parseSsaid(String filename, String searchPackage) throws XmlPullParserException, IOException {
|
||||
try{
|
||||
FileInputStream stream = new FileInputStream(filename);
|
||||
return parseSsaidBinary(stream, searchPackage);
|
||||
}
|
||||
catch (XmlPullParserException e){
|
||||
FileInputStream stream = new FileInputStream(filename);
|
||||
return parseSsaidText(stream, searchPackage);
|
||||
}
|
||||
}
|
||||
@SuppressLint("HardwareIds")
|
||||
public static String getAndroidIdOfApp(Context ctx, String packageId) {
|
||||
if(Build.VERSION.SDK_INT >= 26){
|
||||
try{
|
||||
String[] files = Root.findFiles("/data/system", "settings_ssaid.xml");
|
||||
String ssaidCacheFile = new File(ctx.getCacheDir(), "settings_ssaid.xml").getAbsolutePath();
|
||||
|
||||
for(String ssaidFile : files){
|
||||
try {
|
||||
Root.copyChmodAndChown(ssaidFile, ssaidCacheFile, 777, String.valueOf(Helper.getMyUid(ctx)));
|
||||
return parseSsaid(ssaidCacheFile, packageId);
|
||||
} catch (Exception e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e){
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return Settings.Secure.getString(ctx.getContentResolver(), "android_id");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,9 +1,6 @@
|
|||
package com.psmreborn.nopsmdrm;
|
||||
package com.psmreborn.shared;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -11,6 +8,8 @@ import javax.crypto.*;
|
|||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.*;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
@ -20,29 +19,17 @@ public class StringEncryptor {
|
|||
private static final byte[] iv = {-126, -30, -6, -75, -99, -117, -66, 117, 39, -65, -126, -27, -12, 38, -99, 86};
|
||||
private static final byte[] salt = {-92, -102, -105, -123, 71, -33, 69, -39, -27, -32, 21, 33, 126, -81, 69, 59, 57, 29, -83, -15};
|
||||
|
||||
private Context ctx;
|
||||
private String android_id = null;
|
||||
private int psm_uid = 0;
|
||||
|
||||
StringEncryptor(Context ctx){
|
||||
this.ctx = ctx;
|
||||
public StringEncryptor(String androidId, int psmUid) {
|
||||
this.android_id = androidId;
|
||||
this.psm_uid = psmUid;
|
||||
}
|
||||
|
||||
public String getAndroidId(){
|
||||
return Settings.Secure.getString(ctx.getContentResolver(), "android_id");
|
||||
}
|
||||
public int getPsmUid() {
|
||||
try{
|
||||
ApplicationInfo psmAppInfo = this.ctx.getPackageManager().getApplicationInfo("com.playstation.psstore", 0);
|
||||
if(psmAppInfo != null) {
|
||||
return psmAppInfo.uid;
|
||||
}
|
||||
}
|
||||
catch (PackageManager.NameNotFoundException e) { };
|
||||
return 0;
|
||||
}
|
||||
private String base64(byte[] data){
|
||||
private String encodeBase64(byte[] data){
|
||||
return Base64.encodeToString(data, Base64.DEFAULT);
|
||||
}
|
||||
|
||||
private byte[] decodeBase64(String str) {return Base64.decode(str, Base64.DEFAULT);}
|
||||
public String encryptString(String str) {
|
||||
byte[] data = str.getBytes();
|
||||
byte[] encryptedData = encrypt(data);
|
||||
|
@ -51,12 +38,44 @@ public class StringEncryptor {
|
|||
encodedData[encodedData.length - 2] = 1;
|
||||
encodedData[encodedData.length - 1] = 1;
|
||||
|
||||
return base64(encodedData);
|
||||
return encodeBase64(encodedData);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private byte[] encrypt(byte[] input){
|
||||
public String decryptString(String str, String def) {
|
||||
if(str == null) return def;
|
||||
|
||||
byte[] data = decodeBase64(str);
|
||||
byte[] arrayData = Arrays.copyOf(data, data.length - 2);
|
||||
|
||||
byte[] decryptedData = decrypt(arrayData);
|
||||
if(decryptedData != null){
|
||||
try {
|
||||
return new String(decryptedData, "UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e("STRINGENCRYPTOR", e.toString());
|
||||
return def;
|
||||
}
|
||||
}
|
||||
return def;
|
||||
}
|
||||
public byte[] decrypt(byte[] input){
|
||||
if(input == null) return null;
|
||||
try {
|
||||
Cipher cipher = generateKeyCipher(StringEncryptor.salt, StringEncryptor.iv, Cipher.DECRYPT_MODE);
|
||||
if (cipher != null) {
|
||||
return cipher.doFinal(input);
|
||||
}
|
||||
else {
|
||||
Log.e("STRINGENCRYPTOR", "cipher was null");
|
||||
}
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) { System.out.println(e.toString()) ;}
|
||||
return null;
|
||||
|
||||
}
|
||||
public byte[] encrypt(byte[] input){
|
||||
if(input == null) return null;
|
||||
try {
|
||||
Cipher cipher = generateKeyCipher(StringEncryptor.salt, StringEncryptor.iv, Cipher.ENCRYPT_MODE);
|
||||
if (cipher != null) {
|
||||
|
@ -72,8 +91,8 @@ public class StringEncryptor {
|
|||
|
||||
private Cipher generateKeyCipher(final byte[] salt, final byte[] iv, final int opmode) {
|
||||
try {
|
||||
final String androidId = getAndroidId();
|
||||
final String psmUid = String.valueOf(getPsmUid());
|
||||
final String androidId = this.android_id;
|
||||
final String psmUid = String.valueOf(this.psm_uid);
|
||||
if (androidId == null || psmUid == null) {
|
||||
throw new InvalidParameterException();
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 19 KiB |
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -2,23 +2,14 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:paddingTop="15dp"
|
||||
android:paddingLeft="15dp"
|
||||
android:paddingRight="15dp"
|
||||
android:paddingBottom="15dp"
|
||||
android:keepScreenOn="true"
|
||||
android:noHistory="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/installPsm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black"
|
||||
android:onClick="installStart"
|
||||
android:textSize="30sp"
|
||||
android:enabled="false"
|
||||
android:text="Install NoPsmDrm"/>
|
||||
android:orientation="vertical"
|
||||
android:background="@color/black">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/errorMsg"
|
||||
|
@ -26,6 +17,38 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:text="Checking, Please wait ..."/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/installPsm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="installStart"
|
||||
android:textSize="30sp"
|
||||
android:enabled="false"
|
||||
android:layout_below="@+id/errorMsg"
|
||||
android:textColor="@color/white"
|
||||
android:text="Waiting..."/>
|
||||
<Button
|
||||
android:id="@+id/takeOwnership"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="givePsmOwnership"
|
||||
android:textSize="30sp"
|
||||
android:enabled="false"
|
||||
android:layout_below="@+id/installPsm"
|
||||
android:text=""/>
|
||||
android:textColor="@color/white"
|
||||
android:text="Fix permissions" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/dumpGames"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="dumpAllRifs"
|
||||
android:textSize="30sp"
|
||||
android:enabled="false"
|
||||
android:layout_below="@+id/takeOwnership"
|
||||
android:textColor="@color/white"
|
||||
android:text="Backup all licenses" />
|
||||
|
||||
</RelativeLayout>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -5,7 +5,7 @@
|
|||
<color name="purple_700">#FF3700B3</color>
|
||||
<color name="teal_200">#FF03DAC5</color>
|
||||
<color name="teal_700">#FF018786</color>
|
||||
<color name="gray">#80808080</color>
|
||||
<color name="gray">#8f8f8f8f</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
</resources>
|
|
@ -1,3 +1,11 @@
|
|||
<resources>
|
||||
<string name="app_name">NoPsmDrm Installer</string>
|
||||
<string name="app_name">NoPssDrm</string>
|
||||
<string name="name_nopsmdrm">NoPsmDrm</string>
|
||||
<string name="name_nops1drm_ticket">NoPs1Drm - Ticket Generator</string>
|
||||
<string name="name_nops1drm_update">NoPs1Drm - zPAK Updater</string>
|
||||
<string name="default_email">nopsmdrm@transrights.lgbt</string>
|
||||
<string name="default_password">password</string>
|
||||
<string name="default_online_id">TransgenderPS1</string>
|
||||
<string name="default_account_id">0123456789abcdef</string>
|
||||
<string name="psm_app_package_id">com.playstation.psstore</string>
|
||||
</resources>
|
|
@ -3,4 +3,3 @@ plugins {
|
|||
id 'com.android.application' version '8.3.2' apply false
|
||||
id 'com.android.library' version '8.3.2' apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,12 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
android.useAndroidX=false
|
||||
# Enables namespacing of each library's R class so that its R class includes only the
|
||||
# resources declared in the library itself and none from the library's dependencies,
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonTransitiveRClass=true
|
||||
# make it work how it used to ??
|
||||
android.nonFinalResIds=false
|
||||
|
||||
applicationName = NoPssDrm
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,28 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.rosstonovsky.abxutils'
|
||||
//noinspection GradleDependency needs to work properly on android 2.x
|
||||
compileSdk 10
|
||||
|
||||
defaultConfig {
|
||||
minSdk 10
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//noinspection GradleDependency new version has min sdk 14
|
||||
implementation 'com.android.support:support-core-utils:25.0.0'
|
||||
}
|
|
@ -0,0 +1,924 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.ATTRIBUTE;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_BOOLEAN_FALSE;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_BOOLEAN_TRUE;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_BYTES_BASE64;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_BYTES_HEX;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_DOUBLE;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_FLOAT;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_INT;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_INT_HEX;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_LONG;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_LONG_HEX;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_NULL;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_STRING;
|
||||
import static com.rosstonovsky.abxutils.BinaryXmlSerializer.TYPE_STRING_INTERNED;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class BinaryXmlPullParser implements TypedXmlPullParser {
|
||||
public FastDataInput mIn;
|
||||
private int mCurrentToken = START_DOCUMENT;
|
||||
private int mCurrentDepth = 0;
|
||||
private String mCurrentName;
|
||||
private String mCurrentText;
|
||||
private static final int BUFFER_SIZE = 32_768;
|
||||
|
||||
/**
|
||||
* Pool of attributes parsed for the currently tag. All interactions should
|
||||
* be done via {@link #obtainAttribute()}, findAttribute(String),
|
||||
* and {@link #resetAttributes()}.
|
||||
*/
|
||||
private int mAttributeCount = 0;
|
||||
private Attribute[] mAttributes;
|
||||
|
||||
@Override
|
||||
public void setInput(InputStream is, String encoding) throws XmlPullParserException {
|
||||
if (encoding != null && !"UTF-8".equalsIgnoreCase(encoding)) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (mIn != null) {
|
||||
try {
|
||||
mIn.close();
|
||||
} catch (IOException e) {
|
||||
throw new XmlPullParserException(e.toString());
|
||||
}
|
||||
mIn = null;
|
||||
}
|
||||
mIn = new FastDataInput(is, BUFFER_SIZE);
|
||||
mCurrentToken = START_DOCUMENT;
|
||||
mCurrentDepth = 0;
|
||||
mCurrentName = null;
|
||||
mCurrentText = null;
|
||||
mAttributeCount = 0;
|
||||
mAttributes = new Attribute[8];
|
||||
for (int i = 0; i < mAttributes.length; i++) {
|
||||
mAttributes[i] = new Attribute();
|
||||
}
|
||||
try {
|
||||
final byte[] magic = new byte[4];
|
||||
mIn.readFully(magic);
|
||||
if (!Arrays.equals(magic, PROTOCOL_MAGIC_VERSION_0)) {
|
||||
throw new IOException("Unexpected magic " + bytesToHexString(magic));
|
||||
}
|
||||
// We're willing to immediately consume a START_DOCUMENT if present,
|
||||
// but we're okay if it's missing
|
||||
if (peekNextExternalToken() == START_DOCUMENT) {
|
||||
consumeToken();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new XmlPullParserException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInput(Reader in) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int next() throws XmlPullParserException, IOException {
|
||||
while (true) {
|
||||
final int token = nextToken();
|
||||
switch (token) {
|
||||
case START_TAG:
|
||||
case END_TAG:
|
||||
case END_DOCUMENT:
|
||||
return token;
|
||||
case TEXT:
|
||||
consumeAdditionalText();
|
||||
// Per interface docs, empty text regions are skipped
|
||||
if (mCurrentText != null && mCurrentText.length() != 0) {
|
||||
return TEXT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextToken() throws XmlPullParserException, IOException {
|
||||
if (mCurrentToken == XmlPullParser.END_TAG) {
|
||||
mCurrentDepth--;
|
||||
}
|
||||
int token;
|
||||
try {
|
||||
token = peekNextExternalToken();
|
||||
consumeToken();
|
||||
} catch (EOFException e) {
|
||||
token = END_DOCUMENT;
|
||||
}
|
||||
if (token == XmlPullParser.START_TAG) {
|
||||
// We need to peek forward to find the next external token so
|
||||
// that we parse all pending INTERNAL_ATTRIBUTE tokens
|
||||
peekNextExternalToken();
|
||||
mCurrentDepth++;
|
||||
}
|
||||
mCurrentToken = token;
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek at the next "external" token without consuming it.
|
||||
* <p>
|
||||
* External tokens, such as {@link #START_TAG}, are expected by typical
|
||||
* {@link XmlPullParser} clients. In contrast, internal tokens, such as
|
||||
* ATTRIBUTE, are not expected by typical clients.
|
||||
* <p>
|
||||
* This method consumes any internal events until it reaches the next
|
||||
* external event.
|
||||
*/
|
||||
private int peekNextExternalToken() throws IOException, XmlPullParserException {
|
||||
while (true) {
|
||||
final int token = peekNextToken();
|
||||
if (token == ATTRIBUTE) {
|
||||
consumeToken();
|
||||
continue;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek at the next token in the underlying stream without consuming it.
|
||||
*/
|
||||
private int peekNextToken() throws IOException {
|
||||
return mIn.peekByte() & 0x0f;
|
||||
}
|
||||
|
||||
public int getNextEvent() throws IOException {
|
||||
return mIn.readByte();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and consume the next token in the underlying stream.
|
||||
*/
|
||||
private void consumeToken() throws IOException, XmlPullParserException {
|
||||
final int event = mIn.readByte();
|
||||
final int token = event & 0x0f;
|
||||
final int type = event & 0xf0;
|
||||
switch (token) {
|
||||
case ATTRIBUTE: {
|
||||
final Attribute attr = obtainAttribute();
|
||||
attr.name = mIn.readInternedUTF();
|
||||
attr.type = type;
|
||||
switch (type) {
|
||||
case TYPE_NULL:
|
||||
case TYPE_BOOLEAN_TRUE:
|
||||
case TYPE_BOOLEAN_FALSE:
|
||||
// Nothing extra to fill in
|
||||
break;
|
||||
case TYPE_STRING:
|
||||
|
||||
attr.valueString = mIn.readUTF();
|
||||
break;
|
||||
case TYPE_STRING_INTERNED:
|
||||
attr.valueString = mIn.readInternedUTF();
|
||||
break;
|
||||
case TYPE_BYTES_HEX:
|
||||
case TYPE_BYTES_BASE64:
|
||||
final int len = mIn.readUnsignedShort();
|
||||
final byte[] res = new byte[len];
|
||||
mIn.readFully(res);
|
||||
attr.valueBytes = res;
|
||||
break;
|
||||
case TYPE_INT:
|
||||
case TYPE_INT_HEX:
|
||||
attr.valueInt = mIn.readInt();
|
||||
break;
|
||||
case TYPE_LONG:
|
||||
case TYPE_LONG_HEX:
|
||||
attr.valueLong = mIn.readLong();
|
||||
break;
|
||||
case TYPE_FLOAT:
|
||||
attr.valueFloat = mIn.readFloat();
|
||||
break;
|
||||
case TYPE_DOUBLE:
|
||||
attr.valueDouble = mIn.readDouble();
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unexpected data type " + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case START_DOCUMENT:
|
||||
case XmlPullParser.END_DOCUMENT: {
|
||||
mCurrentName = null;
|
||||
mCurrentText = null;
|
||||
if (mAttributeCount > 0) resetAttributes();
|
||||
break;
|
||||
}
|
||||
case XmlPullParser.START_TAG: {
|
||||
mCurrentName = mIn.readInternedUTF();
|
||||
mCurrentText = null;
|
||||
if (mAttributeCount > 0) resetAttributes();
|
||||
break;
|
||||
}
|
||||
case XmlPullParser.END_TAG: {
|
||||
mCurrentName = mIn.readInternedUTF();
|
||||
mCurrentText = null;
|
||||
//if (mAttributeCount > 0) resetAttributes();
|
||||
break;
|
||||
}
|
||||
case XmlPullParser.TEXT:
|
||||
case XmlPullParser.CDSECT:
|
||||
case XmlPullParser.PROCESSING_INSTRUCTION:
|
||||
case XmlPullParser.COMMENT:
|
||||
case XmlPullParser.DOCDECL:
|
||||
case XmlPullParser.IGNORABLE_WHITESPACE: {
|
||||
mCurrentName = null;
|
||||
mCurrentText = mIn.readUTF();
|
||||
if (mAttributeCount > 0) resetAttributes();
|
||||
break;
|
||||
}
|
||||
case XmlPullParser.ENTITY_REF: {
|
||||
mCurrentName = mIn.readUTF();
|
||||
mCurrentText = resolveEntity(mCurrentName);
|
||||
if (mAttributeCount > 0) resetAttributes();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new IOException("Unknown token " + token + " with type " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the current tag is {@link #TEXT}, consume all subsequent "text"
|
||||
* events, as described by {@link #next}. When finished, the current event
|
||||
* will still be {@link #TEXT}.
|
||||
*/
|
||||
private void consumeAdditionalText() throws IOException, XmlPullParserException {
|
||||
StringBuilder combinedText = new StringBuilder(mCurrentText);
|
||||
while (true) {
|
||||
final int token = peekNextExternalToken();
|
||||
switch (token) {
|
||||
case COMMENT:
|
||||
case PROCESSING_INSTRUCTION:
|
||||
// Quietly consumed
|
||||
consumeToken();
|
||||
break;
|
||||
case TEXT:
|
||||
case CDSECT:
|
||||
case ENTITY_REF:
|
||||
// Additional text regions collected
|
||||
consumeToken();
|
||||
combinedText.append(mCurrentText);
|
||||
break;
|
||||
default:
|
||||
// Next token is something non-text, so wrap things up
|
||||
mCurrentToken = TEXT;
|
||||
mCurrentName = null;
|
||||
mCurrentText = combinedText.toString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull
|
||||
String resolveEntity(@NonNull String entity)
|
||||
throws XmlPullParserException {
|
||||
switch (entity) {
|
||||
case "lt":
|
||||
return "<";
|
||||
case "gt":
|
||||
return ">";
|
||||
case "amp":
|
||||
return "&";
|
||||
case "apos":
|
||||
return "'";
|
||||
case "quot":
|
||||
return "\"";
|
||||
}
|
||||
if (entity.length() > 1 && entity.charAt(0) == '#') {
|
||||
final char c = (char) Integer.parseInt(entity.substring(1));
|
||||
return String.valueOf(c);
|
||||
}
|
||||
throw new XmlPullParserException("Unknown entity " + entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void require(int type, String namespace, String name)
|
||||
throws XmlPullParserException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
if (mCurrentToken != type || !mCurrentName.equals(name)) {
|
||||
throw new XmlPullParserException(getPositionDescription());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String nextText() throws XmlPullParserException, IOException {
|
||||
if (getEventType() != START_TAG) {
|
||||
throw new XmlPullParserException(getPositionDescription());
|
||||
}
|
||||
int eventType = next();
|
||||
if (eventType == TEXT) {
|
||||
String result = getText();
|
||||
eventType = next();
|
||||
if (eventType != END_TAG) {
|
||||
throw new XmlPullParserException(getPositionDescription());
|
||||
}
|
||||
return result;
|
||||
} else if (eventType == END_TAG) {
|
||||
return "";
|
||||
} else {
|
||||
throw new XmlPullParserException(getPositionDescription());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int nextTag() throws XmlPullParserException, IOException {
|
||||
int eventType = next();
|
||||
if (eventType == TEXT && isWhitespace()) {
|
||||
eventType = next();
|
||||
}
|
||||
if (eventType != START_TAG && eventType != END_TAG) {
|
||||
throw new XmlPullParserException(getPositionDescription());
|
||||
}
|
||||
return eventType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate and return a new {@link Attribute} associated with the tag being
|
||||
* currently processed. This will automatically grow the internal pool as
|
||||
* needed.
|
||||
*/
|
||||
public @NonNull
|
||||
Attribute obtainAttribute() {
|
||||
if (mAttributeCount == mAttributes.length) {
|
||||
final int before = mAttributes.length;
|
||||
final int after = before + (before >> 1);
|
||||
mAttributes = Arrays.copyOf(mAttributes, after);
|
||||
for (int i = before; i < after; i++) {
|
||||
mAttributes[i] = new Attribute();
|
||||
}
|
||||
}
|
||||
return mAttributes[mAttributeCount++];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any {@link Attribute} instances that have been allocated by
|
||||
* {@link #obtainAttribute()}, returning them into the pool for recycling.
|
||||
*/
|
||||
public void resetAttributes() {
|
||||
for (int i = 0; i < mAttributeCount; i++) {
|
||||
mAttributes[i].reset();
|
||||
}
|
||||
mAttributeCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributeIndex(String namespace, @NonNull String name) {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
for (int i = 0; i < mAttributeCount; i++) {
|
||||
if (mAttributes[i].name.equals( name)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public Attribute[] getAttributes() {
|
||||
return mAttributes;
|
||||
}
|
||||
|
||||
public String getAttributeValue(String name) {
|
||||
final int index = getAttributeIndex(null, name);
|
||||
if (index != -1) {
|
||||
return mAttributes[index].getValueString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeValue(String namespace, String name) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index != -1) {
|
||||
return mAttributes[index].getValueString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeValue(int index) {
|
||||
return mAttributes[index].getValueString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public byte[] getAttributeBytesHex(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueBytesHex();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public byte[] getAttributeBytesBase64(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueBytesBase64();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributeInt(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributeIntHex(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueIntHex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAttributeLong(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getAttributeLongHex(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueLongHex();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getAttributeFloat(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueFloat();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getAttributeDouble(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueDouble();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAttributeBoolean(int index) throws XmlPullParserException {
|
||||
return mAttributes[index].getValueBoolean();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText() {
|
||||
return mCurrentText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] getTextCharacters(int[] holderForStartAndLength) {
|
||||
final char[] chars = mCurrentText.toCharArray();
|
||||
holderForStartAndLength[0] = 0;
|
||||
holderForStartAndLength[1] = chars.length;
|
||||
return chars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getInputEncoding() {
|
||||
return "UTF-8";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return mCurrentDepth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPositionDescription() {
|
||||
// Not very helpful, but it's the best information we have
|
||||
return "Token " + mCurrentToken + " at depth " + mCurrentDepth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLineNumber() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnNumber() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWhitespace() throws XmlPullParserException {
|
||||
switch (mCurrentToken) {
|
||||
case IGNORABLE_WHITESPACE:
|
||||
return true;
|
||||
case TEXT:
|
||||
case CDSECT:
|
||||
return !TextUtils.isGraphic(mCurrentText);
|
||||
default:
|
||||
throw new XmlPullParserException("Not applicable for token " + mCurrentToken);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
switch (mCurrentToken) {
|
||||
case START_TAG:
|
||||
case END_TAG:
|
||||
// Namespaces are unsupported
|
||||
return NO_NAMESPACE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return mCurrentName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
// Prefixes are not supported
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmptyElementTag() throws XmlPullParserException {
|
||||
if (mCurrentToken == START_TAG) {
|
||||
try {
|
||||
return (peekNextExternalToken() == END_TAG);
|
||||
} catch (IOException e) {
|
||||
throw new XmlPullParserException(e.toString());
|
||||
}
|
||||
}
|
||||
throw new XmlPullParserException("Not at START_TAG");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAttributeCount() {
|
||||
return mAttributeCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeNamespace(int index) {
|
||||
// Namespaces are unsupported
|
||||
return NO_NAMESPACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeName(int index) {
|
||||
return mAttributes[index].name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributePrefix(int index) {
|
||||
// Prefixes are not supported
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAttributeType(int index) {
|
||||
// Validation is not supported
|
||||
return "CDATA";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAttributeDefault(int index) {
|
||||
// Validation is not supported
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEventType() {
|
||||
return mCurrentToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNamespaceCount(int depth) {
|
||||
// Namespaces are unsupported
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespacePrefix(int pos) {
|
||||
// Namespaces are unsupported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespaceUri(int pos) {
|
||||
// Namespaces are unsupported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace(String prefix) {
|
||||
// Namespaces are unsupported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void defineEntityReplacementText(String entityName, String replacementText) {
|
||||
// Custom entities are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeature(String name, boolean state) {
|
||||
// Features are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFeature(String name) {
|
||||
// Features are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(String name, Object value) {
|
||||
// Properties are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
// Properties are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static IllegalArgumentException illegalNamespace() {
|
||||
throw new IllegalArgumentException("Namespaces are not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Holder representing a single attribute. This design enables object
|
||||
* recycling without resorting to autoboxing.
|
||||
* <p>
|
||||
* To support conversion between human-readable XML and binary XML, the
|
||||
* various accessor methods will transparently convert from/to
|
||||
* human-readable values when needed.
|
||||
*/
|
||||
public static class Attribute {
|
||||
public String name;
|
||||
public int type;
|
||||
public String valueString;
|
||||
public byte[] valueBytes;
|
||||
public int valueInt;
|
||||
public long valueLong;
|
||||
public float valueFloat;
|
||||
public double valueDouble;
|
||||
|
||||
public void reset() {
|
||||
name = null;
|
||||
valueString = null;
|
||||
valueBytes = null;
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
String getValueString() {
|
||||
switch (type) {
|
||||
case TYPE_NULL:
|
||||
return null;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
return valueString;
|
||||
case TYPE_BYTES_HEX:
|
||||
return bytesToHexString(valueBytes);
|
||||
case TYPE_BYTES_BASE64:
|
||||
return Base64.encodeToString(valueBytes, Base64.NO_WRAP);
|
||||
case TYPE_INT:
|
||||
return Integer.toString(valueInt);
|
||||
case TYPE_INT_HEX:
|
||||
return Integer.toString(valueInt, 16);
|
||||
case TYPE_LONG:
|
||||
return Long.toString(valueLong);
|
||||
case TYPE_LONG_HEX:
|
||||
return Long.toString(valueLong, 16);
|
||||
case TYPE_FLOAT:
|
||||
return Float.toString(valueFloat);
|
||||
case TYPE_DOUBLE:
|
||||
return Double.toString(valueDouble);
|
||||
case TYPE_BOOLEAN_TRUE:
|
||||
return "true";
|
||||
case TYPE_BOOLEAN_FALSE:
|
||||
return "false";
|
||||
default:
|
||||
// Unknown data type; null is the best we can offer
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
byte[] getValueBytesHex() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_NULL:
|
||||
return null;
|
||||
case TYPE_BYTES_HEX:
|
||||
case TYPE_BYTES_BASE64:
|
||||
return valueBytes;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return hexStringToBytes(valueString);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable
|
||||
byte[] getValueBytesBase64() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_NULL:
|
||||
return null;
|
||||
case TYPE_BYTES_HEX:
|
||||
case TYPE_BYTES_BASE64:
|
||||
return valueBytes;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Base64.decode(valueString, Base64.NO_WRAP);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public int getValueInt() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_INT:
|
||||
case TYPE_INT_HEX:
|
||||
return valueInt;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Integer.parseInt(valueString);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public int getValueIntHex() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_INT:
|
||||
case TYPE_INT_HEX:
|
||||
return valueInt;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Integer.parseInt(valueString, 16);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public long getValueLong() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_LONG:
|
||||
case TYPE_LONG_HEX:
|
||||
return valueLong;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Long.parseLong(valueString);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public long getValueLongHex() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_LONG:
|
||||
case TYPE_LONG_HEX:
|
||||
return valueLong;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Long.parseLong(valueString, 16);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public float getValueFloat() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_FLOAT:
|
||||
return valueFloat;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Float.parseFloat(valueString);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public double getValueDouble() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_DOUBLE:
|
||||
return valueDouble;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
try {
|
||||
return Double.parseDouble(valueString);
|
||||
} catch (Exception e) {
|
||||
throw new XmlPullParserException("Invalid attribute " + name + ": " + e);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getValueBoolean() throws XmlPullParserException {
|
||||
switch (type) {
|
||||
case TYPE_BOOLEAN_TRUE:
|
||||
return true;
|
||||
case TYPE_BOOLEAN_FALSE:
|
||||
return false;
|
||||
case TYPE_STRING:
|
||||
case TYPE_STRING_INTERNED:
|
||||
if ("true".equalsIgnoreCase(valueString)) {
|
||||
return true;
|
||||
} else if ("false".equalsIgnoreCase(valueString)) {
|
||||
return false;
|
||||
} else {
|
||||
throw new XmlPullParserException(
|
||||
"Invalid attribute " + name + ": " + valueString);
|
||||
}
|
||||
default:
|
||||
throw new XmlPullParserException("Invalid conversion from " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: To support unbundled clients, we include an inlined copy
|
||||
// of hex conversion logic from HexDump below
|
||||
private final static char[] HEX_DIGITS =
|
||||
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
|
||||
|
||||
private static int toByte(char c) {
|
||||
if (c >= '0' && c <= '9') return (c - '0');
|
||||
if (c >= 'A' && c <= 'F') return (c - 'A' + 10);
|
||||
if (c >= 'a' && c <= 'f') return (c - 'a' + 10);
|
||||
throw new IllegalArgumentException("Invalid hex char '" + c + "'");
|
||||
}
|
||||
|
||||
static String bytesToHexString(byte[] value) {
|
||||
final int length = value.length;
|
||||
final char[] buf = new char[length * 2];
|
||||
int bufIndex = 0;
|
||||
for (byte b : value) {
|
||||
buf[bufIndex++] = HEX_DIGITS[(b >>> 4) & 0x0F];
|
||||
buf[bufIndex++] = HEX_DIGITS[b & 0x0F];
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
static byte[] hexStringToBytes(String value) {
|
||||
final int length = value.length();
|
||||
if (length % 2 != 0) {
|
||||
throw new IllegalArgumentException("Invalid hex length " + length);
|
||||
}
|
||||
byte[] buffer = new byte[length / 2];
|
||||
for (int i = 0; i < length; i += 2) {
|
||||
buffer[i / 2] = (byte) ((toByte(value.charAt(i)) << 4)
|
||||
| toByte(value.charAt(i + 1)));
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,395 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import static org.xmlpull.v1.XmlPullParser.CDSECT;
|
||||
import static org.xmlpull.v1.XmlPullParser.COMMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.DOCDECL;
|
||||
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.END_TAG;
|
||||
import static org.xmlpull.v1.XmlPullParser.ENTITY_REF;
|
||||
import static org.xmlpull.v1.XmlPullParser.IGNORABLE_WHITESPACE;
|
||||
import static org.xmlpull.v1.XmlPullParser.PROCESSING_INSTRUCTION;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_DOCUMENT;
|
||||
import static org.xmlpull.v1.XmlPullParser.START_TAG;
|
||||
import static org.xmlpull.v1.XmlPullParser.TEXT;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Writer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Serializer that writes XML documents using a custom binary wire protocol
|
||||
* which benchmarking has shown to be 4.3x faster and use 2.4x less disk space
|
||||
* than {@code Xml.newFastSerializer()} for a typical {@code packages.xml}.
|
||||
* <p>
|
||||
* The high-level design of the wire protocol is to directly serialize the event
|
||||
* stream, while efficiently and compactly writing strongly-typed primitives
|
||||
* delivered through the {@link TypedXmlSerializer} interface.
|
||||
* <p>
|
||||
* Each serialized event is a single byte where the lower half is a normal
|
||||
* {@link XmlPullParser} token and the upper half is an optional data type
|
||||
* signal, such as {@link #TYPE_INT}.
|
||||
* <p>
|
||||
* This serializer has some specific limitations:
|
||||
* <ul>
|
||||
* <li>Only the UTF-8 encoding is supported.
|
||||
* <li>Variable length values, such as {@code byte[]} or {@link String}, are
|
||||
* limited to 65,535 bytes in length. Note that {@link String} values are stored
|
||||
* as UTF-8 on the wire.
|
||||
* <li>Namespaces, prefixes, properties, and options are unsupported.
|
||||
* </ul>
|
||||
*/
|
||||
public final class BinaryXmlSerializer implements TypedXmlSerializer {
|
||||
/**
|
||||
* The wire protocol always begins with a well-known magic value of
|
||||
* {@code ABX_}, representing "Android Binary XML." The final byte is a
|
||||
* version number which may be incremented as the protocol changes.
|
||||
*/
|
||||
public static final byte[] PROTOCOL_MAGIC_VERSION_0 = new byte[]{0x41, 0x42, 0x58, 0x00};
|
||||
|
||||
/**
|
||||
* Internal token which represents an attribute associated with the most
|
||||
* recent {@link #} token.
|
||||
*/
|
||||
public static final int ATTRIBUTE = 15;
|
||||
|
||||
public static final int TYPE_NULL = 1 << 4;
|
||||
public static final int TYPE_STRING = 2 << 4;
|
||||
public static final int TYPE_STRING_INTERNED = 3 << 4;
|
||||
public static final int TYPE_BYTES_HEX = 4 << 4;
|
||||
public static final int TYPE_BYTES_BASE64 = 5 << 4;
|
||||
public static final int TYPE_INT = 6 << 4;
|
||||
public static final int TYPE_INT_HEX = 7 << 4;
|
||||
public static final int TYPE_LONG = 8 << 4;
|
||||
public static final int TYPE_LONG_HEX = 9 << 4;
|
||||
public static final int TYPE_FLOAT = 10 << 4;
|
||||
public static final int TYPE_DOUBLE = 11 << 4;
|
||||
public static final int TYPE_BOOLEAN_TRUE = 12 << 4;
|
||||
public static final int TYPE_BOOLEAN_FALSE = 13 << 4;
|
||||
|
||||
/**
|
||||
* Default buffer size, which matches {@code FastXmlSerializer}. This should
|
||||
* be kept in sync with {@link }.
|
||||
*/
|
||||
private static final int BUFFER_SIZE = 32_768;
|
||||
|
||||
private FastDataOutput mOut;
|
||||
|
||||
/**
|
||||
* Stack of tags which are currently active via {@link #startTag} and which
|
||||
* haven't been terminated via {@link #endTag}.
|
||||
*/
|
||||
private int mTagCount = 0;
|
||||
private String[] mTagNames;
|
||||
|
||||
/**
|
||||
* Write the given token and optional {@link String} into our buffer.
|
||||
*/
|
||||
private void writeToken(int token, String text) throws IOException {
|
||||
if (text != null) {
|
||||
mOut.writeByte(token | TYPE_STRING);
|
||||
mOut.writeUTF(text);
|
||||
} else {
|
||||
mOut.writeByte(token | TYPE_NULL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutput(OutputStream os, String encoding) throws IOException {
|
||||
if (encoding != null && !"UTF-8".equalsIgnoreCase(encoding)) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
mOut = new FastDataOutput(os, BUFFER_SIZE);
|
||||
mOut.write(PROTOCOL_MAGIC_VERSION_0);
|
||||
|
||||
mTagCount = 0;
|
||||
mTagNames = new String[8];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutput(Writer writer) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
mOut.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startDocument(String encoding, Boolean standalone)
|
||||
throws IOException {
|
||||
if (encoding != null && !"UTF-8".equalsIgnoreCase(encoding)) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
if (standalone != null && !standalone) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
mOut.writeByte(START_DOCUMENT | TYPE_NULL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endDocument() throws IOException {
|
||||
mOut.writeByte(END_DOCUMENT | TYPE_NULL);
|
||||
flush();
|
||||
mOut.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDepth() {
|
||||
return mTagCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNamespace() {
|
||||
// Namespaces are unsupported
|
||||
return XmlPullParser.NO_NAMESPACE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return mTagNames[mTagCount - 1];
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer startTag(String namespace, String name) throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
if (mTagCount == mTagNames.length) {
|
||||
mTagNames = Arrays.copyOf(mTagNames, mTagCount + (mTagCount >> 1));
|
||||
}
|
||||
mTagNames[mTagCount++] = name;
|
||||
mOut.writeByte(START_TAG | TYPE_STRING_INTERNED);
|
||||
mOut.writeInternedUTF(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer endTag(String namespace, String name) throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mTagCount--;
|
||||
mOut.writeByte(END_TAG | TYPE_STRING_INTERNED);
|
||||
mOut.writeInternedUTF(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_STRING);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeUTF(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeInterned(String namespace, String name, String value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_STRING_INTERNED);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeInternedUTF(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeBytesHex(String namespace, String name, byte[] value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_HEX);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeShort(value.length);
|
||||
mOut.write(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeBytesBase64(String namespace, String name, byte[] value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_BYTES_BASE64);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeShort(value.length);
|
||||
mOut.write(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeInt(String namespace, String name, int value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_INT);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeInt(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeIntHex(String namespace, String name, int value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_INT_HEX);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeInt(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeLong(String namespace, String name, long value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_LONG);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeLong(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeLongHex(String namespace, String name, long value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_LONG_HEX);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeLong(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeFloat(String namespace, String name, float value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_FLOAT);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeFloat(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeDouble(String namespace, String name, double value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_DOUBLE);
|
||||
mOut.writeInternedUTF(name);
|
||||
mOut.writeDouble(value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer attributeBoolean(String namespace, String name, boolean value)
|
||||
throws IOException {
|
||||
if (namespace != null && !namespace.isEmpty()) throw illegalNamespace();
|
||||
if (value) {
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_TRUE);
|
||||
mOut.writeInternedUTF(name);
|
||||
} else {
|
||||
mOut.writeByte(ATTRIBUTE | TYPE_BOOLEAN_FALSE);
|
||||
mOut.writeInternedUTF(name);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer text(char[] buf, int start, int len) throws IOException {
|
||||
writeToken(TEXT, new String(buf, start, len));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public XmlSerializer text(String text) throws IOException {
|
||||
writeToken(TEXT, text);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cdsect(String text) throws IOException {
|
||||
writeToken(CDSECT, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void entityRef(String text) throws IOException {
|
||||
writeToken(ENTITY_REF, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processingInstruction(String text) throws IOException {
|
||||
writeToken(PROCESSING_INSTRUCTION, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void comment(String text) throws IOException {
|
||||
writeToken(COMMENT, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void docdecl(String text) throws IOException {
|
||||
writeToken(DOCDECL, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ignorableWhitespace(String text) throws IOException {
|
||||
writeToken(IGNORABLE_WHITESPACE, text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFeature(String name, boolean state) {
|
||||
// Quietly handle no-op features
|
||||
if ("http://xmlpull.org/v1/doc/features.html#indent-output".equals(name)) {
|
||||
return;
|
||||
}
|
||||
// Features are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getFeature(String name) {
|
||||
// Features are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(String name, Object value) {
|
||||
// Properties are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getProperty(String name) {
|
||||
// Properties are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrefix(String prefix, String namespace) {
|
||||
// Prefixes are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix(String namespace, boolean generatePrefix) {
|
||||
// Prefixes are not supported
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static IllegalArgumentException illegalNamespace() {
|
||||
throw new IllegalArgumentException("Namespaces are not supported");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import android.support.annotation.*;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.DataInput;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
public class FastDataInput implements DataInput, Closeable {
|
||||
private static final int MAX_UNSIGNED_SHORT = 65_535;
|
||||
|
||||
private InputStream mIn;
|
||||
|
||||
private final byte[] mBuffer;
|
||||
private final int mBufferCap;
|
||||
private int mBufferPos;
|
||||
private int mBufferLim;
|
||||
/**
|
||||
* Values that have been "interned" by {@link #readInternedUTF()}.
|
||||
*/
|
||||
private int mStringRefCount = 0;
|
||||
private String[] mStringRefs = new String[32];
|
||||
|
||||
public FastDataInput(@NonNull InputStream in, int bufferSize) {
|
||||
mIn = in;
|
||||
if (bufferSize < 8) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
mBuffer = new byte[bufferSize];
|
||||
mBufferCap = mBuffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a {@link FastDataInput} to potentially be recycled. You must not
|
||||
* interact with the object after releasing it.
|
||||
*/
|
||||
public void release() {
|
||||
mIn = null;
|
||||
mBufferPos = 0;
|
||||
mBufferLim = 0;
|
||||
mStringRefCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-initializes the object for the new input.
|
||||
*/
|
||||
private void setInput(@NonNull InputStream in) {
|
||||
mIn = in;
|
||||
mBufferPos = 0;
|
||||
mBufferLim = 0;
|
||||
mStringRefCount = 0;
|
||||
}
|
||||
|
||||
private void fill(int need) throws IOException {
|
||||
final int remain = mBufferLim - mBufferPos;
|
||||
System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain);
|
||||
mBufferPos = 0;
|
||||
mBufferLim = remain;
|
||||
need -= remain;
|
||||
while (need > 0) {
|
||||
int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim);
|
||||
if (c == -1) {
|
||||
throw new EOFException();
|
||||
} else {
|
||||
mBufferLim += c;
|
||||
need -= c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mIn.close();
|
||||
release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFully(byte[] b) throws IOException {
|
||||
readFully(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readFully(byte[] b, int off, int len) throws IOException {
|
||||
// Attempt to read directly from buffer space if there's enough room,
|
||||
// otherwise fall back to chunking into place
|
||||
if (mBufferCap >= len) {
|
||||
if (mBufferLim - mBufferPos < len) fill(len);
|
||||
System.arraycopy(mBuffer, mBufferPos, b, off, len);
|
||||
mBufferPos += len;
|
||||
} else {
|
||||
final int remain = mBufferLim - mBufferPos;
|
||||
System.arraycopy(mBuffer, mBufferPos, b, off, remain);
|
||||
mBufferPos += remain;
|
||||
off += remain;
|
||||
len -= remain;
|
||||
while (len > 0) {
|
||||
int c = mIn.read(b, off, len);
|
||||
if (c == -1) {
|
||||
throw new EOFException();
|
||||
} else {
|
||||
off += c;
|
||||
len -= c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readUTF() throws IOException {
|
||||
final int len = readUnsignedShort();
|
||||
byte[] tmp = new byte[len];
|
||||
readFully(tmp);
|
||||
return new String(tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a {@link String} value with the additional signal that the given
|
||||
* value is a candidate for being canonicalized, similar to
|
||||
* {@link String#intern()}.
|
||||
* <p>
|
||||
* Canonicalization is implemented by writing each unique string value once
|
||||
* the first time it appears, and then writing a lightweight {@code short}
|
||||
* reference when that string is written again in the future.
|
||||
*/
|
||||
public @NonNull
|
||||
String readInternedUTF() throws IOException {
|
||||
final int ref = readUnsignedShort();
|
||||
if (ref == MAX_UNSIGNED_SHORT) {
|
||||
final String s = readUTF();
|
||||
// We can only safely intern when we have remaining values; if we're
|
||||
// full we at least sent the string value above
|
||||
if (mStringRefCount < MAX_UNSIGNED_SHORT) {
|
||||
if (mStringRefCount == mStringRefs.length) {
|
||||
mStringRefs = Arrays.copyOf(mStringRefs,
|
||||
mStringRefCount + (mStringRefCount >> 1));
|
||||
}
|
||||
mStringRefs[mStringRefCount++] = s;
|
||||
}
|
||||
return s;
|
||||
} else {
|
||||
return mStringRefs[ref];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readBoolean() throws IOException {
|
||||
return readByte() != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the same decoded value as {@link #readByte()} but without
|
||||
* actually consuming the underlying data.
|
||||
*/
|
||||
public byte peekByte() throws IOException {
|
||||
if (mBufferLim - mBufferPos < 1) fill(1);
|
||||
return mBuffer[mBufferPos];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte readByte() throws IOException {
|
||||
if (mBufferLim - mBufferPos < 1) fill(1);
|
||||
return mBuffer[mBufferPos++];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readUnsignedByte() throws IOException {
|
||||
return (int)(readByte() & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short readShort() throws IOException {
|
||||
if (mBufferLim - mBufferPos < 2) fill(2);
|
||||
return (short) (((mBuffer[mBufferPos++] & 0xff) << 8) |
|
||||
((mBuffer[mBufferPos++] & 0xff)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readUnsignedShort() throws IOException {
|
||||
return (int)(readShort() & 0xffff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public char readChar() throws IOException {
|
||||
return (char) readShort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int readInt() throws IOException {
|
||||
if (mBufferLim - mBufferPos < 4) fill(4);
|
||||
return (((mBuffer[mBufferPos++] & 0xff) << 24) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 16) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 8) |
|
||||
((mBuffer[mBufferPos++] & 0xff)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readLong() throws IOException {
|
||||
if (mBufferLim - mBufferPos < 8) fill(8);
|
||||
int h = ((mBuffer[mBufferPos++] & 0xff) << 24) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 16) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 8) |
|
||||
((mBuffer[mBufferPos++] & 0xff));
|
||||
int l = ((mBuffer[mBufferPos++] & 0xff) << 24) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 16) |
|
||||
((mBuffer[mBufferPos++] & 0xff) << 8) |
|
||||
((mBuffer[mBufferPos++] & 0xff));
|
||||
return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float readFloat() throws IOException {
|
||||
return Float.intBitsToFloat(readInt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public double readDouble() throws IOException {
|
||||
return Double.longBitsToDouble(readLong());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int skipBytes(int n) {
|
||||
// Callers should read data piecemeal
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readLine() {
|
||||
// Callers should read data piecemeal
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.DataOutput;
|
||||
import java.io.Flushable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class FastDataOutput implements DataOutput, Flushable, Closeable {
|
||||
private static final int MAX_UNSIGNED_SHORT = 65_535;
|
||||
|
||||
private final OutputStream mOut;
|
||||
|
||||
private final byte[] mBuffer;
|
||||
private final int mBufferCap;
|
||||
private int mBufferPos;
|
||||
|
||||
/**
|
||||
* Values that have been "interned" by {@link #writeInternedUTF(String)}.
|
||||
*/
|
||||
private final HashMap<String, Short> mStringRefs = new HashMap<>();
|
||||
|
||||
public FastDataOutput(OutputStream out, int bufferSize) {
|
||||
mOut = out;
|
||||
if (bufferSize < 8) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
mBuffer = new byte[bufferSize];
|
||||
mBufferCap = mBuffer.length;
|
||||
}
|
||||
|
||||
private void drain() throws IOException {
|
||||
if (mBufferPos > 0) {
|
||||
mOut.write(mBuffer, 0, mBufferPos);
|
||||
mBufferPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
drain();
|
||||
mOut.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
mOut.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
writeByte(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (mBufferCap < len) {
|
||||
drain();
|
||||
mOut.write(b, off, len);
|
||||
} else {
|
||||
if (mBufferCap - mBufferPos < len) drain();
|
||||
System.arraycopy(b, off, mBuffer, mBufferPos, len);
|
||||
mBufferPos += len;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeUTF(String s) throws IOException {
|
||||
// Attempt to write directly to buffer space if there's enough room,
|
||||
// otherwise fall back to chunking into place
|
||||
if (mBufferCap - mBufferPos < 2 + s.length()) drain();
|
||||
byte[] b = s.getBytes("UTF-8");
|
||||
writeShort(b.length);
|
||||
write(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a {@link String} value with the additional signal that the given
|
||||
* value is a candidate for being canonicalized, similar to
|
||||
* {@link String#intern()}.
|
||||
* <p>
|
||||
* Canonicalization is implemented by writing each unique string value once
|
||||
* the first time it appears, and then writing a lightweight {@code short}
|
||||
* reference when that string is written again in the future.
|
||||
*/
|
||||
public void writeInternedUTF(String s) throws IOException {
|
||||
Short ref = mStringRefs.get(s);
|
||||
if (ref != null) {
|
||||
writeShort(ref);
|
||||
} else {
|
||||
writeShort(MAX_UNSIGNED_SHORT);
|
||||
writeUTF(s);
|
||||
// We can only safely intern when we have remaining values; if we're
|
||||
// full we at least sent the string value above
|
||||
ref = (short) mStringRefs.size();
|
||||
if (mStringRefs.size() < MAX_UNSIGNED_SHORT) {
|
||||
mStringRefs.put(s, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBoolean(boolean v) throws IOException {
|
||||
writeByte(v ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeByte(int v) throws IOException {
|
||||
if (mBufferCap - mBufferPos < 1) drain();
|
||||
mBuffer[mBufferPos++] = (byte) ((v) & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeShort(int v) throws IOException {
|
||||
if (mBufferCap - mBufferPos < 2) drain();
|
||||
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((v) & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeChar(int v) throws IOException {
|
||||
writeShort((short) v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeInt(int v) throws IOException {
|
||||
if (mBufferCap - mBufferPos < 4) drain();
|
||||
mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((v >> 8) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((v) & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLong(long v) throws IOException {
|
||||
if (mBufferCap - mBufferPos < 8) drain();
|
||||
int i = (int) (v >> 32);
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i) & 0xff);
|
||||
i = (int) v;
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i >> 8) & 0xff);
|
||||
mBuffer[mBufferPos++] = (byte) ((i) & 0xff);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFloat(float v) throws IOException {
|
||||
writeInt(Float.floatToIntBits(v));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDouble(double v) throws IOException {
|
||||
writeLong(Double.doubleToLongBits(v));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBytes(String s) {
|
||||
// Callers should use writeUTF()
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeChars(String s) {
|
||||
// Callers should use writeUTF()
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import android.support.annotation.*;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
/**
|
||||
* Specialization of {@link XmlPullParser} which adds explicit methods to
|
||||
* support consistent and efficient conversion of primitive data types.
|
||||
*/
|
||||
public interface TypedXmlPullParser extends XmlPullParser {
|
||||
/**
|
||||
* @return index of requested attribute, otherwise {@code -1} if undefined
|
||||
*/
|
||||
default int getAttributeIndex(@Nullable String namespace, @NonNull String name) {
|
||||
final boolean namespaceNull = (namespace == null);
|
||||
final int count = getAttributeCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
if ((namespaceNull || namespace.equals(getAttributeNamespace(i)))
|
||||
&& name.equals(getAttributeName(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return index of requested attribute
|
||||
* @throws XmlPullParserException if the value is undefined
|
||||
*/
|
||||
default int getAttributeIndexOrThrow(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) {
|
||||
throw new XmlPullParserException("Missing attribute " + name);
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
@NonNull byte[] getAttributeBytesHex(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
@NonNull byte[] getAttributeBytesBase64(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
int getAttributeInt(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
int getAttributeIntHex(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
long getAttributeLong(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
long getAttributeLongHex(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
float getAttributeFloat(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
double getAttributeDouble(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed
|
||||
*/
|
||||
boolean getAttributeBoolean(int index) throws XmlPullParserException;
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default @NonNull byte[] getAttributeBytesHex(@Nullable String namespace,
|
||||
@NonNull String name) throws XmlPullParserException {
|
||||
return getAttributeBytesHex(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default @NonNull byte[] getAttributeBytesBase64(@Nullable String namespace,
|
||||
@NonNull String name) throws XmlPullParserException {
|
||||
return getAttributeBytesBase64(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default int getAttributeInt(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeInt(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeIntHex(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default long getAttributeLong(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeLong(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeLongHex(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default float getAttributeFloat(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeFloat(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default double getAttributeDouble(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeDouble(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}
|
||||
* @throws XmlPullParserException if the value is malformed or undefined
|
||||
*/
|
||||
default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
|
||||
throws XmlPullParserException {
|
||||
return getAttributeBoolean(getAttributeIndexOrThrow(namespace, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default @Nullable byte[] getAttributeBytesHex(@Nullable String namespace,
|
||||
@NonNull String name, @Nullable byte[] defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeBytesHex(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default @Nullable byte[] getAttributeBytesBase64(@Nullable String namespace,
|
||||
@NonNull String name, @Nullable byte[] defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeBytesBase64(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default int getAttributeInt(@Nullable String namespace, @NonNull String name,
|
||||
int defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeInt(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
|
||||
int defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeIntHex(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default long getAttributeLong(@Nullable String namespace, @NonNull String name,
|
||||
long defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeLong(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
|
||||
long defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeLongHex(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
|
||||
float defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeFloat(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
|
||||
double defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeDouble(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return decoded strongly-typed {@link #getAttributeValue}, otherwise
|
||||
* default value if the value is malformed or undefined
|
||||
*/
|
||||
default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
|
||||
boolean defaultValue) {
|
||||
final int index = getAttributeIndex(namespace, name);
|
||||
if (index == -1) return defaultValue;
|
||||
try {
|
||||
return getAttributeBoolean(index);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (C) 2023 rosstonovsky
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.rosstonovsky.abxutils;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Specialization of {@link XmlSerializer} which adds explicit methods to
|
||||
* support consistent and efficient conversion of primitive data types.
|
||||
*
|
||||
*/
|
||||
public interface TypedXmlSerializer extends XmlSerializer {
|
||||
/**
|
||||
* Functionally equivalent to {@link #attribute(String, String, String)} but
|
||||
* with the additional signal that the given value is a candidate for being
|
||||
* canonicalized, similar to {@link String#intern()}.
|
||||
*/
|
||||
XmlSerializer attributeInterned( String namespace, String name,
|
||||
String value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeBytesHex( String namespace, String name,
|
||||
byte[] value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeBytesBase64( String namespace, String name,
|
||||
byte[] value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeInt( String namespace, String name,
|
||||
int value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeIntHex( String namespace, String name,
|
||||
int value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeLong( String namespace, String name,
|
||||
long value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeLongHex( String namespace, String name,
|
||||
long value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeFloat( String namespace, String name,
|
||||
float value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeDouble( String namespace, String name,
|
||||
double value) throws IOException;
|
||||
|
||||
/**
|
||||
* Encode the given strongly-typed value and serialize using
|
||||
* {@link #attribute(String, String, String)}.
|
||||
*/
|
||||
XmlSerializer attributeBoolean( String namespace, String name,
|
||||
boolean value) throws IOException;
|
||||
}
|
|
@ -4,6 +4,7 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace 'eu.chainfire.libsuperuser'
|
||||
//noinspection GradleDependency needs to work properly on android 2.x
|
||||
compileSdk 10
|
||||
|
||||
defaultConfig {
|
||||
|
@ -12,7 +13,7 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
@ -22,6 +23,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation 'androidx.annotation:annotation-jvm:1.7.1'
|
||||
//noinspection GradleDependency new version has min sdk 14
|
||||
implementation 'com.android.support:support-core-utils:25.0.0'
|
||||
}
|
|
@ -20,9 +20,8 @@ import android.content.Context;
|
|||
import android.os.Handler;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Base application class to extend from, solving some issues with
|
||||
|
|
|
@ -20,9 +20,8 @@ import android.os.Looper;
|
|||
import android.util.Log;
|
||||
import android.os.Process;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.support.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Utility class for logging and debug features that (by default) does nothing when not in debug mode
|
||||
|
|
|
@ -20,9 +20,8 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.support.annotation.*;
|
||||
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@AnyThread
|
||||
|
|
|
@ -19,10 +19,8 @@ package eu.chainfire.libsuperuser;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.support.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for modifying SELinux policies, reducing the number of calls to a minimum.
|
||||
|
|
|
@ -42,10 +42,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import java.lang.Object;
|
||||
import java.lang.String;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.support.annotation.*;
|
||||
import eu.chainfire.libsuperuser.StreamGobbler.OnLineListener;
|
||||
import eu.chainfire.libsuperuser.StreamGobbler.OnStreamClosedListener;
|
||||
|
||||
|
|
|
@ -23,10 +23,8 @@ import java.io.InputStreamReader;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.support.annotation.*;
|
||||
|
||||
|
||||
/**
|
||||
* Thread utility class continuously reading from an InputStream
|
||||
|
|
|
@ -21,9 +21,7 @@ import android.os.Build;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.support.annotation.*;
|
||||
|
||||
/**
|
||||
* Utility class to decide between toolbox and toybox calls on M.
|
||||
|
|
66
readme.md
66
readme.md
|
@ -1,17 +1,35 @@
|
|||
-- Credits
|
||||
-- How to use it?
|
||||
|
||||
- frangarj for the original NoPsmDrm on PSVita https://github.com/frangarcj/NoPsmDrm/
|
||||
- Chaser - an absolute legend who still had their old XPLAY with PSM running on it *still going* all these years later, could never have done it without ya!
|
||||
- Li, for being the single person who has done the most research into PSM out of anyone ever ..
|
||||
- Whomever was around before me, Thanks for keeping the dream alive..
|
||||
Your device must be rooted & on android 2.3.0 or later.
|
||||
|
||||
-- Installing games
|
||||
Install the NoPssDrm.apk file;
|
||||
Approve all permissions and grant it root access.
|
||||
|
||||
so you just put nopsmdrm dumps into /sdcard/Android/data/com.playstation.psstore/files/psm, and then they show up and you can run them.
|
||||
- If your device is not PlayStation Certified, you will have to "Certify" it first,
|
||||
the NoPssDrm app itself can do this, just press the "PS Certify" button.
|
||||
|
||||
- If you do not have the PlayStation Mobile app installed, you will see a "Install PSM" button.
|
||||
this will download the PSM.apk from psmreborn.com, and then install it onto your phone.
|
||||
(this automatically does the android 14+ ``--bypass-low-target-sdk-block`` if needed.)
|
||||
|
||||
- Once both PSM and the PlayStation Certificates are installed, press the "Install NoPsmDrm" button.
|
||||
this will install the actual NoPsmDrm patches to the app, and allow PSM games to be played with it.
|
||||
|
||||
You can then just place NoPsmDrm backups of games into /sdcard/data/android/com.playstation.psstore/files/psm ...
|
||||
|
||||
the format Vita NoPsmDrm backups come in usually contains an RW/ and RO/ folder,
|
||||
which isn't present on the Android version. you can just move all the files from RO and RW
|
||||
into the games Title ID folder (the NPXAXXXX one..), and that should get it working
|
||||
|
||||
-- "PlayStation Suite" / PSP & PS1 Games
|
||||
When starting one of these games you'll be given the option to launch either with ``PlayStation Mobile`` or ``NoPs1Drm``
|
||||
you just need to select the ``NoPs1Drm`` option, both times, and the game should launch assuming you have the correct matching APK & ZPAK pair
|
||||
for the game, and the game can actually run on your device.
|
||||
|
||||
-- known issues:
|
||||
- Games only run with wifi off .. (i think its trying to check for updates or something..)
|
||||
- FAKE.rif from PSVita dont work ..
|
||||
- 8008103F error code when starting -- on some devices, wifi is required to be turned on when starting the app for the first time.
|
||||
this is not actually accessing a server and its just so it can read your Mac Address, which PSM needs for (something???)
|
||||
|
||||
-- source code?
|
||||
|
||||
|
@ -23,8 +41,36 @@ the library.db has had the following trigger added :
|
|||
```
|
||||
CREATE TRIGGER CONTENT_ID_MEMES AFTER INSERT ON LibraryTable
|
||||
BEGIN
|
||||
UPDATE LibraryTable SET content_id="UM0105-" || title_id || "-0000000000000000" WHERE title_id = new.title_id;
|
||||
UPDATE LibraryTable SET content_id="UM0105-" || title_id || "_00-0000000000000000" WHERE title_id = new.title_id;
|
||||
END;
|
||||
```
|
||||
|
||||
... the rest should be in the installer apk code so :d
|
||||
|
||||
``com.playstation.playstationcertified.jar``
|
||||
is taken from an xperia play, however its an empty JAR file with only a class constructor in it and nothing else.
|
||||
|
||||
NP Ticket generation is done via a library created by olebeck; see https://github.com/olebeck/npticketjava
|
||||
|
||||
|
||||
-- Permissions explaination
|
||||
|
||||
SuperUser - This app works by modifying the internal data for the PSM application (at /data/data/com.playstation.psstore) so it sees it as if you have already logged into the app and setup an account, then it patches libdefault and libpsmkdc for nopsmdrm-like patches,
|
||||
|
||||
there seems to be some obfuscation going on with libdefault that makes it not work correctly on a modified APK, and besides to have the ability to backup already installed games will always require root access.
|
||||
|
||||
Storage (read/write) - If you have PSM already installed and activated, the first thing the app will do is backup all your PSM applications internal data files to your SDCard, before modifying any of it, this requires the external storage permission ...
|
||||
|
||||
Phone calls - This is litterally, just to read your IMEI number, you see PsmDrm derives a "Console ID" using a bunch of unique identifiers on your phone, one of which, is your IMEI number, and you need the entire Phone permission to read the IMEI on android 2.2.X, for some reason!
|
||||
|
||||
com.playstation.psscertified - this is a specical permission given to be able to access the pscertified framework, which is used to check if your device is "PlayStation Certified".
|
||||
|
||||
-- Credits
|
||||
|
||||
- frangarj, for the original NoPsmDrm on PSVita https://github.com/frangarcj/NoPsmDrm/
|
||||
- Chaser, an absolute legend who still had their old XPLAY with PSM running on it *still going* all these years later, could never have done it without ya!
|
||||
- random(), helping us work out what was wrong on Android 5+
|
||||
- rosstonovsky, this random library for reading android's binary XML files; https://github.com/rosstonovsky/ABXUtils/tree/master/abxutils
|
||||
- Li, for being the single person who has done the most research into PSM out of anyone ever ..
|
||||
- ele7enxxh, for the hooking library that actually works on android 2.2.X https://github.com/ele7enxxh/Android-Inline-Hook/tree/master
|
||||
- olebeck, for their knowledge of NP Tickets, and how to generate them https://github.com/olebeck/npticketjava
|
||||
- Whomever was around before me, Thanks for keeping the dream alive..
|
||||
|
|
|
@ -10,9 +10,16 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven {
|
||||
name = "olebeck-silica.codes"
|
||||
url = uri("https://silica.codes/api/packages/olebeck/maven")
|
||||
}
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
}
|
||||
}
|
||||
}
|
||||
rootProject.name = "nopsmdrm"
|
||||
rootProject.name = "nopssdrm"
|
||||
include ':app'
|
||||
include ':libsuperuser'
|
||||
include ':libABX'
|
||||
|
|
Loading…
Reference in New Issue