Compare commits

...

47 Commits
v1.3 ... main

Author SHA1 Message Date
Li a6dc9fbd93 oops forgot this 2024-05-20 09:07:01 +00:00
Li 83b155437f Merge branch 'main' of https://silica.codes/Li/NoPsmDrm-Android 2024-05-20 14:37:32 +12:00
Li 6fcf0643c7 supress warnings in gradle 2024-05-20 14:37:18 +12:00
Li 5dfdea82d0 Update readme.md 2024-05-20 02:11:46 +00:00
random() d3838fdb28 Update readme.md 2024-05-19 23:25:22 +00:00
Li 3305933dc9 Update readme.md 2024-05-19 23:18:41 +00:00
Li d904e40b77 Update readme.md 2024-05-19 23:18:09 +00:00
Li a416efaebc Fix some issues, skip failed to download message in PSM if zpak exists .. 2024-05-20 10:59:34 +12:00
Li 513f30b1da aa 2024-05-19 18:30:52 +12:00
Li 584e6c913d Remove AndroidX 2024-05-19 17:59:42 +12:00
Li 6e157330cf Fix crashing (temporary fix) 2024-05-19 15:29:12 +12:00
Li 0fce254917 catch more possible errors from PSM 2024-05-19 15:23:00 +12:00
Li f821147c21 Make game start even if ZPAK download fails.: 2024-05-19 14:18:57 +12:00
Li f7555574d6 Make np ticket generation more correct -ty olebeck 2024-05-19 13:07:40 +12:00
Li 6acd9fb269 Add NoPs1Drm !! 2024-05-17 17:32:46 +12:00
Li 780278c9fa aaa 2024-05-16 14:07:50 +12:00
Li 8bc60d1083 Update Busybox to a newer one 2024-05-15 18:14:06 +12:00
Li 68e38ba358 Merge branch 'main' of https://silica.codes/Li/NoPsmDrm-Android 2024-05-13 21:46:39 +12:00
Li 9fae0324af update to use modern android apis ig 2024-05-13 21:46:20 +12:00
Li 08b6881b63 Update Modern-Android.md 2024-05-11 05:16:43 +00:00
random() 3e3dfb1a50 Update readme.md 2024-05-10 22:14:50 +00:00
Li 1b93b198b2 Merge branch 'main' of https://silica.codes/Li/NoPsmDrm-Android 2024-05-11 03:02:10 +12:00
Li bba0b4e3ea allow parsing plaintext xml file too 2024-05-11 02:54:50 +12:00
Li 997c50ec3e Update readme.md 2024-05-10 14:31:11 +00:00
Li d6ab6ee992 Update readme.md 2024-05-10 14:29:23 +00:00
Li 9eb8331d85 Remove androidmanifest 2024-05-11 02:15:33 +12:00
Li 9da9468621 update modern android guide 2024-05-11 02:09:09 +12:00
Li d53984fa48 make it all work on android 5.x 2024-05-11 02:04:59 +12:00
Li 076ec14d2b Add fix permissions button 2024-05-10 21:39:41 +12:00
Li 295cd7458d Make it work on android 5.x onwards (Working even on android 14!!!! 2024-05-10 21:07:36 +12:00
Li 2a9098c8c0 update version number (again) 2024-04-30 14:11:24 +12:00
Li 0d774e35af update version number 2024-04-30 14:09:58 +12:00
Li dd58d20549 fix race condition 2024-04-30 13:59:59 +12:00
Li b363371fe6 add ability to install even if psm is installed to sdcard 2024-04-29 15:11:30 +12:00
Li 75ee1d7b16 improve backup function 2024-04-29 00:52:34 +12:00
Li 50180c78e1 add stuffs 2024-04-28 10:42:14 +12:00
Li d8d7134c1e add 2024-04-28 01:00:11 +12:00
Li 017012413f Add menu? 2024-04-28 00:53:24 +12:00
Li c4cddf38c6 Make nice icon 2024-04-27 22:14:22 +12:00
Li 7420b7b661 Merge branch 'main' of https://silica.codes/Li/NoPsmDrm-Android 2024-04-26 00:25:51 +12:00
Li e56db07fd7 Make it more compatible(?) 2024-04-26 00:25:39 +12:00
Li 0dc733ac8d Update readme.md 2024-04-25 04:50:38 +00:00
Li 5f6a88068a Update readme.md 2024-04-25 04:45:34 +00:00
Li 27c90807e6 Update readme.md 2024-04-25 04:41:56 +00:00
Li edd951acae Update readme.md 2024-04-25 04:39:18 +00:00
Li be9ddbdc04 Update readme.md 2024-04-25 04:36:46 +00:00
Li 648c741ee0 Update readme.md 2024-04-25 03:23:32 +00:00
62 changed files with 4467 additions and 451 deletions

4
.gitignore vendored
View File

@ -16,4 +16,6 @@
local.properties
libsuperuser/build/*
app/build/*
app/release/*
app/release/*
libNpTicket/*

View File

@ -1 +1 @@
nopsmdrm
nopssdrm

View File

@ -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>

177
LICENSE Normal file
View File

@ -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

50
Modern-Android.md Normal file
View File

@ -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.

View File

@ -4,16 +4,16 @@ plugins {
android {
namespace 'com.psmreborn.nopsmdrm'
//noinspection GradleDependency were targeting the xperia play
//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 3
versionName "1.3"
//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 {
@ -25,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'
}

View File

@ -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="10">
<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>

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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) { };
}
}

View File

@ -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)");
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,47 +0,0 @@
package com.psmreborn.nopsmdrm;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import eu.chainfire.libsuperuser.Shell;
public class Helper {
private static Context ctx;
public static void setContext(Context context){
ctx = context;
}
public static boolean isNoPsmDrmAlreadyInstalled() {
try {
if(new File(getPsmApp().nativeLibraryDir, "libdefault_real.so").exists()){
return true;
}
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return false;
}
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;
}
}

View File

@ -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();
}
}
}

View File

@ -1,24 +1,33 @@
package com.psmreborn.nopsmdrm;
import static com.psmreborn.nopsmdrm.Helper.*;
import static com.psmreborn.nopsmdrm.Root.*;
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,154 +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" +
"<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");
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);
@ -257,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();
@ -274,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;
}
@ -284,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)
@ -294,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();
}
}
}

View File

@ -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,
});
}
}
}

View File

@ -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 -9 '" + 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();
}
}

View File

@ -1,48 +1,75 @@
package com.psmreborn.nopsmdrm;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
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 java.io.File;
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;
}
@Override
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;
}
Root.setContext(ctx);
Permissions.requestPermissions(this.ctx);
Helper.killPsm(ctx);
} catch (Exception e) {
wasError = true;
@ -56,18 +83,23 @@ public class Startup extends AsyncTask<Void, Void, Void> {
TextView statusTV = (TextView) ((Activity)ctx).findViewById(R.id.errorMsg);
Button installButton = (Button) ((Activity)ctx).findViewById(R.id.installPsm);
if(!wasError) {
statusTV.setText("");
handler.post(() -> {
statusTV.setText("");
if(isNoPsmDrmAlreadyInstalled()){
installButton.setText("Update NoPsmDrm");
};
installButton.setEnabled(true);
if(Helper.isNoPsmDrmInstalled(ctx)){
setPsmInstalled(ctx);
};
updateNames(ctx);
installButton.setEnabled(true);
});
}
else{
statusTV.setText("Error: "+errorMsg);
handler.post(() -> {
statusTV.setText("Error: " + errorMsg);
});
}
}
}

View File

@ -1,4 +1,4 @@
package com.psmreborn.nopsmdrm.pscertified;
package com.psmreborn.pscertified;
public class CertifiedDeviceEntry {

View File

@ -1,4 +1,4 @@
package com.psmreborn.nopsmdrm.pscertified;
package com.psmreborn.pscertified;
public class CertifiedDeviceList {
private String manufacturer = null;

View File

@ -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;

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}
}

View File

@ -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

View File

@ -8,7 +8,8 @@
android:paddingBottom="15dp"
android:keepScreenOn="true"
android:noHistory="true"
android:orientation="vertical">
android:orientation="vertical"
android:background="@color/black">
<TextView
android:id="@+id/errorMsg"
@ -22,10 +23,32 @@
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:layout_below="@+id/errorMsg"
android:text="Install NoPsmDrm"/>
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: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.

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -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

1
libABX/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

28
libABX/build.gradle Normal file
View File

@ -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'
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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'
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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;

View File

@ -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

View File

@ -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.

View File

@ -1,22 +1,35 @@
-- How to use it?
Your device must be rooted & it must be on android 4.4.2 or older,
Your device must be rooted & on android 2.3.0 or later.
Install the PlayStation Mobile App :
https://web.archive.org/web/20150311233351/http://playstation.com/psm/store/en.html, if you do not already have it.
Install the NoPssDrm.apk file;
Approve all permissions and grant it root access.
then install the NoPsmDrm Android app, and click the "Install" button. grant it root permission and you should be good to go!
- 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.
Put NoPsmDrm backups of games into /sdcard/android/com.playstation.psstore/files/psm ...
- 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..)
- 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?
@ -32,13 +45,32 @@ UPDATE LibraryTable SET content_id="UM0105-" || title_id || "_00-000000000000000
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!
- 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..
- ele7enxxh for the hooking library that actually works on android 2.2.X https://github.com/ele7enxxh/Android-Inline-Hook/tree/master

View File

@ -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'