From bed479f7a3c0faf77b318739b4e6c681925cddb4 Mon Sep 17 00:00:00 2001 From: Grishka Date: Thu, 22 Feb 2024 21:35:46 +0300 Subject: [PATCH 1/6] QR codes for profiles --- mastodon/build.gradle | 5 + mastodon/src/main/AndroidManifest.xml | 4 + .../google/android/gms/common/api/Status.aidl | 3 + .../common/api/internal/IStatusCallback.aidl | 7 + .../gms/common/internal/ConnectionInfo.aidl | 7 + .../common/internal/GetServiceRequest.aidl | 3 + .../gms/common/internal/IGmsCallbacks.aidl | 15 + .../common/internal/IGmsServiceBroker.aidl | 10 + .../ModuleAvailabilityResponse.aidl | 3 + .../ModuleInstallIntentResponse.aidl | 3 + .../moduleinstall/ModuleInstallResponse.aidl | 3 + .../ModuleInstallStatusUpdate.aidl | 3 + .../internal/ApiFeatureRequest.aidl | 3 + .../internal/IModuleInstallCallbacks.aidl | 13 + .../internal/IModuleInstallService.aidl | 14 + .../IModuleInstallStatusListener.aidl | 7 + .../google/android/gms/common/Feature.java | 15 + .../google/android/gms/common/api/Scope.java | 13 + .../google/android/gms/common/api/Status.java | 33 + .../gms/common/internal/ConnectionInfo.java | 19 + .../common/internal/GetServiceRequest.java | 47 ++ .../ModuleAvailabilityResponse.java | 13 + .../ModuleInstallIntentResponse.java | 13 + .../moduleinstall/ModuleInstallResponse.java | 21 + .../ModuleInstallStatusUpdate.java | 63 ++ .../internal/ApiFeatureRequest.java | 21 + .../android/fragments/ProfileFragment.java | 90 ++- .../fragments/ProfileQrCodeFragment.java | 583 ++++++++++++++++++ .../android/fragments/ThreadFragment.java | 2 +- .../googleservices/ConnectionResult.java | 47 ++ .../android/googleservices/GmsClient.java | 116 ++++ .../barcodescanner/Barcode.java | 253 ++++++++ .../barcodescanner/BarcodeScanner.java | 38 ++ .../ui/drawables/FancyQrCodeDrawable.java | 149 +++++ .../RadialParticleSystemDrawable.java | 133 ++++ .../ui/views/FixedAspectRatioFrameLayout.java | 57 ++ .../ui/wrapstodon/RoundedImageView.java | 74 +++ .../main/res/drawable/ic_download_20px.xml | 9 + .../src/main/res/drawable/ic_qr_code_20px.xml | 9 + .../res/drawable/ic_qr_code_scanner_24px.xml | 9 + mastodon/src/main/res/drawable/rect_24dp.xml | 5 + .../src/main/res/layout/fragment_profile.xml | 15 + .../main/res/layout/fragment_profile_qr.xml | 132 ++++ .../main/res/layout/profile_qr_toolbar.xml | 7 + 44 files changed, 2075 insertions(+), 14 deletions(-) create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/api/Status.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/api/internal/IStatusCallback.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/internal/ConnectionInfo.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/internal/GetServiceRequest.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsCallbacks.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallCallbacks.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallService.aidl create mode 100644 mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallStatusListener.aidl create mode 100644 mastodon/src/main/java/com/google/android/gms/common/Feature.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/api/Scope.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/api/Status.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/internal/ConnectionInfo.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.java create mode 100644 mastodon/src/main/java/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/googleservices/ConnectionResult.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/googleservices/GmsClient.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/Barcode.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/BarcodeScanner.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/drawables/FancyQrCodeDrawable.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/drawables/RadialParticleSystemDrawable.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioFrameLayout.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/wrapstodon/RoundedImageView.java create mode 100644 mastodon/src/main/res/drawable/ic_download_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_qr_code_20px.xml create mode 100644 mastodon/src/main/res/drawable/ic_qr_code_scanner_24px.xml create mode 100644 mastodon/src/main/res/drawable/rect_24dp.xml create mode 100644 mastodon/src/main/res/layout/fragment_profile_qr.xml create mode 100644 mastodon/src/main/res/layout/profile_qr_toolbar.xml diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 1e38bdd096..509232b512 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -134,6 +134,9 @@ android { buildFeatures { buildConfig true } + buildFeatures{ + aidl true + } } dependencies { @@ -151,6 +154,8 @@ dependencies { implementation 'org.jsoup:jsoup:1.14.3' implementation 'com.squareup:otto:1.3.8' implementation 'de.psdev:async-otto:1.0.3' + implementation 'com.google.zxing:core:3.5.3' + implementation 'org.microg:safe-parcel:1.5.0' implementation 'org.parceler:parceler-api:1.1.12' implementation 'com.github.bottom-software-foundation:bottom-java:2.1.0' annotationProcessor 'org.parceler:parceler:1.1.12' diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 08ab4ec893..2e70b43836 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -39,6 +39,10 @@ android:windowSoftInputMode="adjustPan" android:largeHeap="true"> + + diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/api/Status.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/api/Status.aidl new file mode 100644 index 0000000000..701f99a914 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/api/Status.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.api; + +parcelable Status; \ No newline at end of file diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/api/internal/IStatusCallback.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/api/internal/IStatusCallback.aidl new file mode 100644 index 0000000000..6355a13b4a --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/api/internal/IStatusCallback.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.common.api.internal; + +import com.google.android.gms.common.api.Status; + +interface IStatusCallback { + void onResult(in Status status); +} diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/internal/ConnectionInfo.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/internal/ConnectionInfo.aidl new file mode 100644 index 0000000000..b393f11d0e --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/internal/ConnectionInfo.aidl @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal; +parcelable ConnectionInfo; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/internal/GetServiceRequest.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/internal/GetServiceRequest.aidl new file mode 100644 index 0000000000..791dbe907f --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/internal/GetServiceRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.internal; + +parcelable GetServiceRequest; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsCallbacks.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsCallbacks.aidl new file mode 100644 index 0000000000..8fe63347d6 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsCallbacks.aidl @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.gms.common.internal; + +import android.os.Bundle; +import com.google.android.gms.common.internal.ConnectionInfo; + +interface IGmsCallbacks { + void onPostInitComplete(int statusCode, IBinder binder, in Bundle params); + void onAccountValidationComplete(int statusCode, in Bundle params); + void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, in ConnectionInfo info); +} diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl new file mode 100644 index 0000000000..a4b747e8fe --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/internal/IGmsServiceBroker.aidl @@ -0,0 +1,10 @@ +package com.google.android.gms.common.internal; + +import android.os.Bundle; + +import com.google.android.gms.common.internal.IGmsCallbacks; +import com.google.android.gms.common.internal.GetServiceRequest; + +interface IGmsServiceBroker { + void getService(IGmsCallbacks callback, in GetServiceRequest request) = 45; +} \ No newline at end of file diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.aidl new file mode 100644 index 0000000000..b44d217427 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.moduleinstall; + +parcelable ModuleAvailabilityResponse; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.aidl new file mode 100644 index 0000000000..18bcfa6e2b --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.moduleinstall; + +parcelable ModuleInstallIntentResponse; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.aidl new file mode 100644 index 0000000000..d5b31230a6 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.moduleinstall; + +parcelable ModuleInstallResponse; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.aidl new file mode 100644 index 0000000000..1669f5eaec --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.moduleinstall; + +parcelable ModuleInstallStatusUpdate; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.aidl new file mode 100644 index 0000000000..2f8571f291 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.aidl @@ -0,0 +1,3 @@ +package com.google.android.gms.common.moduleinstall.internal; + +parcelable ApiFeatureRequest; diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallCallbacks.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallCallbacks.aidl new file mode 100644 index 0000000000..39f51ca491 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallCallbacks.aidl @@ -0,0 +1,13 @@ +package com.google.android.gms.common.moduleinstall.internal; + +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse; +import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse; +import com.google.android.gms.common.moduleinstall.ModuleInstallResponse; + +interface IModuleInstallCallbacks { + void onModuleAvailabilityResponse(in Status status, in ModuleAvailabilityResponse response) = 0; + void onModuleInstallResponse(in Status status, in ModuleInstallResponse response) = 1; + void onModuleInstallIntentResponse(in Status status, in ModuleInstallIntentResponse response) = 2; + void onStatus(in Status status) = 3; +} diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallService.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallService.aidl new file mode 100644 index 0000000000..7737eceda6 --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallService.aidl @@ -0,0 +1,14 @@ +package com.google.android.gms.common.moduleinstall.internal; + +import com.google.android.gms.common.api.internal.IStatusCallback; +import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener; + +interface IModuleInstallService { + void areModulesAvailable(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 0; + void installModules(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request, IModuleInstallStatusListener listener) = 1; + void getInstallModulesIntent(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 2; + void releaseModules(IStatusCallback callback, in ApiFeatureRequest request) = 3; + void unregisterListener(IStatusCallback callback, IModuleInstallStatusListener listener) = 5; +} diff --git a/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallStatusListener.aidl b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallStatusListener.aidl new file mode 100644 index 0000000000..250599035a --- /dev/null +++ b/mastodon/src/main/aidl/com/google/android/gms/common/moduleinstall/internal/IModuleInstallStatusListener.aidl @@ -0,0 +1,7 @@ +package com.google.android.gms.common.moduleinstall.internal; + +import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate; + +interface IModuleInstallStatusListener { + void onModuleInstallStatusUpdate(in ModuleInstallStatusUpdate statusUpdate) = 0; +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/Feature.java b/mastodon/src/main/java/com/google/android/gms/common/Feature.java new file mode 100644 index 0000000000..01bb5b6741 --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/Feature.java @@ -0,0 +1,15 @@ +package com.google.android.gms.common; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class Feature extends AutoSafeParcelable{ + @SafeParceled(1) + public String name; + @SafeParceled(2) + public int oldVersion; + @SafeParceled(3) + public long version=-1; + + public static final Creator CREATOR=new AutoCreator<>(Feature.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/api/Scope.java b/mastodon/src/main/java/com/google/android/gms/common/api/Scope.java new file mode 100644 index 0000000000..5f78394253 --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/api/Scope.java @@ -0,0 +1,13 @@ +package com.google.android.gms.common.api; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class Scope extends AutoSafeParcelable{ + @SafeParceled(1) + public int versionCode=1; + @SafeParceled(2) + public String scopeUri; + + public static final Creator CREATOR=new AutoCreator<>(Scope.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/api/Status.java b/mastodon/src/main/java/com/google/android/gms/common/api/Status.java new file mode 100644 index 0000000000..a029b9992e --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/api/Status.java @@ -0,0 +1,33 @@ +package com.google.android.gms.common.api; + +import android.app.PendingIntent; + +import org.joinmastodon.android.googleservices.ConnectionResult; +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class Status extends AutoSafeParcelable{ + @SafeParceled(1000) + public int versionCode; + @SafeParceled(1) + public int statusCode; + @SafeParceled(2) + public String statusMessage; + @SafeParceled(3) + public PendingIntent pendingIntent; + @SafeParceled(4) + public ConnectionResult connectionResult; + + public static final Creator CREATOR=new AutoCreator<>(Status.class); + + @Override + public String toString(){ + return "Status{"+ + "versionCode="+versionCode+ + ", statusCode="+statusCode+ + ", statusMessage='"+statusMessage+'\''+ + ", pendingIntent="+pendingIntent+ + ", connectionResult="+connectionResult+ + '}'; + } +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/internal/ConnectionInfo.java b/mastodon/src/main/java/com/google/android/gms/common/internal/ConnectionInfo.java new file mode 100644 index 0000000000..2cb2c4e130 --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/internal/ConnectionInfo.java @@ -0,0 +1,19 @@ +package com.google.android.gms.common.internal; + +import android.os.Bundle; + +import com.google.android.gms.common.Feature; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ConnectionInfo extends AutoSafeParcelable{ + @SafeParceled(1) + public Bundle params; + @SafeParceled(2) + public Feature[] features; + @SafeParceled(3) + public int unknown3; + + public static final Creator CREATOR=new AutoCreator<>(ConnectionInfo.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java b/mastodon/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java new file mode 100644 index 0000000000..976c85854f --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/internal/GetServiceRequest.java @@ -0,0 +1,47 @@ +package com.google.android.gms.common.internal; + +import android.os.Bundle; +import android.os.IBinder; +import android.accounts.Account; + +import com.google.android.gms.common.Feature; +import com.google.android.gms.common.api.Scope; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class GetServiceRequest extends AutoSafeParcelable{ + @SafeParceled(1) + int versionCode=6; + @SafeParceled(2) + public int serviceId; + @SafeParceled(3) + public int gmsVersion; + @SafeParceled(4) + public String packageName; + @SafeParceled(5) + public IBinder accountAccessor; + @SafeParceled(6) + public Scope[] scopes; + @SafeParceled(7) + public Bundle extras; + @SafeParceled(8) + public Account account; + @SafeParceled(9) + @Deprecated + long field9; + @SafeParceled(10) + public Feature[] defaultFeatures; + @SafeParceled(11) + public Feature[] apiFeatures; + @SafeParceled(12) + boolean supportsConnectionInfo; + @SafeParceled(13) + int field13; + @SafeParceled(14) + boolean field14; + @SafeParceled(15) + String attributionTag; + + public static final Creator CREATOR=new AutoCreator<>(GetServiceRequest.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.java b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.java new file mode 100644 index 0000000000..8c5c924fef --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleAvailabilityResponse.java @@ -0,0 +1,13 @@ +package com.google.android.gms.common.moduleinstall; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ModuleAvailabilityResponse extends AutoSafeParcelable{ + @SafeParceled(1) + public boolean modulesAvailable; + @SafeParceled(2) + public int availabilityStatus; + + public static final Creator CREATOR=new AutoCreator<>(ModuleAvailabilityResponse.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.java b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.java new file mode 100644 index 0000000000..d1b25e47a0 --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallIntentResponse.java @@ -0,0 +1,13 @@ +package com.google.android.gms.common.moduleinstall; + +import android.app.PendingIntent; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ModuleInstallIntentResponse extends AutoSafeParcelable{ + @SafeParceled(1) + public PendingIntent pendingIntent; + + public static final Creator CREATOR=new AutoCreator<>(ModuleInstallIntentResponse.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.java b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.java new file mode 100644 index 0000000000..bde197ae51 --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallResponse.java @@ -0,0 +1,21 @@ +package com.google.android.gms.common.moduleinstall; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ModuleInstallResponse extends AutoSafeParcelable{ + @SafeParceled(1) + public int sessionID; + @SafeParceled(2) + public boolean shouldUnregisterListener; + + public static final Creator CREATOR=new AutoCreator<>(ModuleInstallResponse.class); + + @Override + public String toString(){ + return "ModuleInstallResponse{"+ + "sessionID="+sessionID+ + ", shouldUnregisterListener="+shouldUnregisterListener+ + '}'; + } +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.java b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.java new file mode 100644 index 0000000000..c0a52379dd --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/ModuleInstallStatusUpdate.java @@ -0,0 +1,63 @@ +package com.google.android.gms.common.moduleinstall; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class ModuleInstallStatusUpdate extends AutoSafeParcelable{ + public static final int STATE_UNKNOWN = 0; + /** + * The request is pending and will be processed soon. + */ + public static final int STATE_PENDING = 1; + /** + * The optional module download is in progress. + */ + public static final int STATE_DOWNLOADING = 2; + /** + * The optional module download has been canceled. + */ + public static final int STATE_CANCELED = 3; + /** + * Installation is completed; the optional modules are available to the client app. + */ + public static final int STATE_COMPLETED = 4; + /** + * The optional module download or installation has failed. + */ + public static final int STATE_FAILED = 5; + /** + * The optional modules have been downloaded and the installation is in progress. + */ + public static final int STATE_INSTALLING = 6; + /** + * The optional module download has been paused. + *

+ * This usually happens when connectivity requirements can't be met during download. Once the connectivity requirements + * are met, the download will be resumed automatically. + */ + public static final int STATE_DOWNLOAD_PAUSED = 7; + + @SafeParceled(1) + public int sessionID; + @SafeParceled(2) + public int installState; + @SafeParceled(3) + public Long bytesDownloaded; + @SafeParceled(4) + public Long totalBytesToDownload; + @SafeParceled(5) + public int errorCode; + + @Override + public String toString(){ + return "ModuleInstallStatusUpdate{"+ + "sessionID="+sessionID+ + ", installState="+installState+ + ", bytesDownloaded="+bytesDownloaded+ + ", totalBytesToDownload="+totalBytesToDownload+ + ", errorCode="+errorCode+ + '}'; + } + + public static final Creator CREATOR=new AutoCreator<>(ModuleInstallStatusUpdate.class); +} diff --git a/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.java b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.java new file mode 100644 index 0000000000..4ff5e38c1a --- /dev/null +++ b/mastodon/src/main/java/com/google/android/gms/common/moduleinstall/internal/ApiFeatureRequest.java @@ -0,0 +1,21 @@ +package com.google.android.gms.common.moduleinstall.internal; + +import com.google.android.gms.common.Feature; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +import java.util.List; + +public class ApiFeatureRequest extends AutoSafeParcelable{ + @SafeParceled(value=1, subClass=Feature.class) + public List features; + @SafeParceled(2) + public boolean urgent; + @SafeParceled(3) + public String sessionId; + @SafeParceled(4) + public String callingPackage; + + public static final Creator CREATOR=new AutoCreator<>(ApiFeatureRequest.class); +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 42d89fcb6f..f1792d9bdb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -28,6 +28,7 @@ import android.text.TextUtils; import android.transition.ChangeBounds; import android.transition.Fade; +import android.transition.Transition; import android.transition.TransitionManager; import android.transition.TransitionSet; import android.view.Gravity; @@ -35,7 +36,6 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; @@ -66,18 +66,15 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetPrivateNote; import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials; -import org.joinmastodon.android.api.requests.instance.GetInstance; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment; import org.joinmastodon.android.fragments.account_list.FollowerListFragment; import org.joinmastodon.android.fragments.account_list.FollowingListFragment; import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment; import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment; -import org.joinmastodon.android.fragments.settings.SettingsServerFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.Attachment; -import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.M3AlertDialogBuilder; @@ -160,6 +157,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private View tabsDivider; private View actionButtonWrap; private CustomDrawingOrderLinearLayout scrollableContent; + private ImageButton qrCodeButton; private Account account, remoteAccount; private String accountID; @@ -275,6 +273,7 @@ public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bu scrollableContent=content.findViewById(R.id.scrollable_content); list=content.findViewById(R.id.metadata); rolesView=content.findViewById(R.id.roles); + qrCodeButton=content.findViewById(R.id.qr_code); avatarBorder.setOutlineProvider(OutlineProviders.roundedRect(26)); avatarBorder.setClipToOutline(true); @@ -435,15 +434,14 @@ public void getOutline(View view, Outline outline){ nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true)); bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true)); - -// qrCodeButton.setOnClickListener(v->{ -// Bundle args=new Bundle(); -// args.putString("account", accountID); -// args.putParcelable("targetAccount", Parcels.wrap(account)); -// ProfileQrCodeFragment qf=new ProfileQrCodeFragment(); -// qf.setArguments(args); -// qf.show(getChildFragmentManager(), "qrDialog"); -// }); + qrCodeButton.setOnClickListener(v->{ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("targetAccount", Parcels.wrap(account)); + ProfileQrCodeFragment qf=new ProfileQrCodeFragment(); + qf.setArguments(args); + qf.show(getChildFragmentManager(), "qrDialog"); + }); return sizeWrapper; } @@ -1201,18 +1199,53 @@ private void enterEditMode(Account account){ toolbar.setNavigationContentDescription(R.string.discard); ViewGroup parent=contentView.findViewById(R.id.scrollable_content); + Runnable updater=new Runnable(){ + @Override + public void run(){ + // setPadding() calls nullLayouts() internally, forcing the text layout to update + actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0); + actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0); + actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY); + actionButton.postOnAnimation(this); + } + }; + actionButton.postOnAnimation(updater); TransitionManager.beginDelayedTransition(parent, new TransitionSet() .addTransition(new Fade(Fade.IN | Fade.OUT)) .addTransition(new ChangeBounds()) .setDuration(250) .setInterpolator(CubicBezierInterpolator.DEFAULT) + .addListener(new Transition.TransitionListener(){ + @Override + public void onTransitionStart(Transition transition){} + + @Override + public void onTransitionEnd(Transition transition){ + actionButton.removeCallbacks(updater); + } + + @Override + public void onTransitionCancel(Transition transition){} + + @Override + public void onTransitionPause(Transition transition){} + + @Override + public void onTransitionResume(Transition transition){} + }) ); name.setVisibility(View.GONE); rolesView.setVisibility(View.GONE); usernameWrap.setVisibility(View.GONE); + name.setVisibility(View.GONE); + username.setVisibility(View.GONE); + name.setVisibility(View.INVISIBLE); + username.setVisibility(View.INVISIBLE); bio.setVisibility(View.GONE); countersLayout.setVisibility(View.GONE); + qrCodeButton.setVisibility(View.GONE); + usernameDomain.setVisibility(View.INVISIBLE); nameEditWrap.setVisibility(View.VISIBLE); nameEdit.setText(account.displayName); @@ -1249,11 +1282,40 @@ private void exitEditMode(){ editSaveMenuItem=null; ViewGroup parent=contentView.findViewById(R.id.scrollable_content); + Runnable updater=new Runnable(){ + @Override + public void run(){ + // setPadding() calls nullLayouts() internally, forcing the text layout to update + actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0); + actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0); + actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY); + actionButton.postOnAnimation(this); + } + }; + actionButton.postOnAnimation(updater); TransitionManager.beginDelayedTransition(parent, new TransitionSet() .addTransition(new Fade(Fade.IN | Fade.OUT)) .addTransition(new ChangeBounds()) .setDuration(250) .setInterpolator(CubicBezierInterpolator.DEFAULT) + .addListener(new Transition.TransitionListener(){ + @Override + public void onTransitionStart(Transition transition){} + + @Override + public void onTransitionEnd(Transition transition){ + actionButton.removeCallbacks(updater); + } + + @Override + public void onTransitionCancel(Transition transition){} + + @Override + public void onTransitionPause(Transition transition){} + + @Override + public void onTransitionResume(Transition transition){} + }) ); nameEditWrap.setVisibility(View.GONE); bioEditWrap.setVisibility(View.GONE); @@ -1266,6 +1328,8 @@ private void exitEditMode(){ pager.setVisibility(View.VISIBLE); tabbar.setVisibility(View.VISIBLE); updateMetadataHeight(); + usernameDomain.setVisibility(View.VISIBLE); + qrCodeButton.setVisibility(View.VISIBLE); InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class); imm.hideSoftInputFromWindow(content.getWindowToken(), 0); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java new file mode 100644 index 0000000000..4c7a1a01c2 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileQrCodeFragment.java @@ -0,0 +1,583 @@ +package org.joinmastodon.android.fragments; + +import android.Manifest; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.DashPathEffect; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.RemoteException; +import android.os.SystemClock; +import android.provider.MediaStore; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.common.Feature; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse; +import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse; +import com.google.android.gms.common.moduleinstall.ModuleInstallResponse; +import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate; +import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.QRCodeWriter; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; + +import org.joinmastodon.android.MainActivity; +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.MastodonAPIController; +import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.googleservices.GmsClient; +import org.joinmastodon.android.googleservices.barcodescanner.Barcode; +import org.joinmastodon.android.googleservices.barcodescanner.BarcodeScanner; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.ui.M3AlertDialogBuilder; +import org.joinmastodon.android.ui.drawables.FancyQrCodeDrawable; +import org.joinmastodon.android.ui.drawables.RadialParticleSystemDrawable; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.FixedAspectRatioFrameLayout; +import org.parceler.Parcels; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import me.grishka.appkit.fragments.AppKitFragment; +import me.grishka.appkit.imageloader.ViewImageLoader; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; +import me.grishka.appkit.utils.CustomViewHelper; +import me.grishka.appkit.utils.V; + +public class ProfileQrCodeFragment extends AppKitFragment{ + private static final String TAG="ProfileQrCodeFragment"; + private static final int PERMISSION_RESULT=388; + private static final int SCAN_RESULT=439; + + private Context themeWrapper; + private GradientDrawable scrim=new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xE6000000, 0xD9000000}); + private RadialParticleSystemDrawable particles; + private View codeContainer; + private View particleAnimContainer; + private Animator currentTransition; + + private String accountID; + private Account account; + private String accountDomain; + private Intent scannerIntent; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + setStyle(STYLE_NO_FRAME, 0); + setHasOptionsMenu(true); + accountID=getArguments().getString("account"); + account=Parcels.unwrap(getArguments().getParcelable("targetAccount")); + setCancelable(false); + scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true); + } + + @Override + public void onStart(){ + super.onStart(); + Dialog dlg=getDialog(); + dlg.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT); + dlg.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR); + dlg.getWindow().setNavigationBarColor(0); + dlg.getWindow().setStatusBarColor(0); + WindowManager.LayoutParams lp=dlg.getWindow().getAttributes(); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){ + lp.layoutInDisplayCutoutMode=WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + } + dlg.getWindow().setAttributes(lp); + if(!isTablet){ + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + } + dlg.setOnKeyListener((dialog, keyCode, event)->{ + if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){ + dismiss(); + } + return true; + }); + } + + @Override + public void onDismiss(DialogInterface dialog){ + super.onDismiss(dialog); + getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); + } + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){ + View content=View.inflate(themeWrapper, R.layout.fragment_profile_qr, container); + View decor=getDialog().getWindow().getDecorView(); + decor.setOnApplyWindowInsetsListener((v, insets)->{ + content.setPadding(insets.getStableInsetLeft(), insets.getStableInsetTop(), insets.getStableInsetRight(), insets.getStableInsetBottom()); + return insets.consumeStableInsets(); + }); + int flags=decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; + flags&=~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + decor.setSystemUiVisibility(flags); + content.setBackground(scrim); + + String url=account.url; + QRCodeWriter writer=new QRCodeWriter(); + BitMatrix code; + try{ + code=writer.encode(url, BarcodeFormat.QR_CODE, 0, 0, Map.of(EncodeHintType.MARGIN, 0, EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H)); + }catch(WriterException e){ + throw new RuntimeException(e); + } + + View codeView=content.findViewById(R.id.code); + ImageView avatar=content.findViewById(R.id.avatar); + TextView username=content.findViewById(R.id.username); + TextView domain=content.findViewById(R.id.domain); + View share=content.findViewById(R.id.share_btn); + Button save=content.findViewById(R.id.save_btn); + View cornerAnimContainer=content.findViewById(R.id.corner_animation_container); + particleAnimContainer=content.findViewById(R.id.particle_animation_container); + codeContainer=content.findViewById(R.id.code_container); + + if(!TextUtils.isEmpty(account.avatar)){ + ViewImageLoader.loadWithoutAnimation(avatar, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(24), V.dp(24), List.of(), Uri.parse(account.avatarStatic))); + } + username.setText(account.username); + String accDomain=account.getDomain(); + domain.setText(accountDomain=TextUtils.isEmpty(accDomain) ? AccountSessionManager.get(accountID).domain : accDomain); + Drawable logo=getResources().getDrawable(R.drawable.ic_ntf_logo, themeWrapper.getTheme()).mutate(); + logo.setTint(UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary)); + codeView.setBackground(new FancyQrCodeDrawable(code, UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary), logo)); + + share.setOnClickListener(v->{ + Intent intent=new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, account.url); + startActivity(Intent.createChooser(intent, getString(R.string.share_user))); + }); + save.setOnClickListener(v->saveCodeAsFile()); + + cornerAnimContainer.setBackground(new AnimatedCornersDrawable(themeWrapper)); + int particleColor=UiUtils.getThemeColor(themeWrapper, R.attr.colorM3Primary); + particles=new RadialParticleSystemDrawable(5000, 200, (particleColor & 0xFFFFFF) | 0x80000000, particleColor & 0xFFFFFF, V.dp(65), V.dp(50), getResources().getDisplayMetrics().density); + particleAnimContainer.setBackground(particles); + + return content; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + if(savedInstanceState==null){ + AnimatorSet set=new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofInt(scrim, "alpha", 0, 255), + ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, V.dp(50), 0), + ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0, 1), + ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0, 1) + ); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.setDuration(350); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + currentTransition=null; + } + }); + currentTransition=set; + set.start(); + } + } + + @Override + public void dismiss(){ + dismissWithAnimation(super::dismiss); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + if(GmsClient.isGooglePlayServicesAvailable(getActivity())){ + MenuItem item=menu.add(0, 0, 0, R.string.scan_qr_code); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + item.setIcon(R.drawable.ic_qr_code_scanner_24px); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){ + startActivityForResult(scannerIntent, SCAN_RESULT); + }else{ + ProgressDialog progress=new ProgressDialog(getActivity()); + progress.setMessage(getString(R.string.loading)); + progress.setCancelable(false); + progress.show(); + GmsClient.getModuleInstallerService(getActivity(), new GmsClient.ServiceConnectionCallback<>(){ + @Override + public void onSuccess(IModuleInstallService service, int connectionID){ + ApiFeatureRequest req=new ApiFeatureRequest(); + req.callingPackage=getActivity().getPackageName(); + Feature feature=new Feature(); + feature.name="mlkit.barcode.ui"; + feature.version=1; + feature.oldVersion=-1; + req.features=List.of(feature); + req.urgent=true; + try{ + service.installModules(new IModuleInstallCallbacks.Stub(){ + @Override + public void onModuleAvailabilityResponse(Status status, ModuleAvailabilityResponse response) throws RemoteException{} + + @Override + public void onModuleInstallResponse(Status status, ModuleInstallResponse response) throws RemoteException{} + + @Override + public void onModuleInstallIntentResponse(Status status, ModuleInstallIntentResponse response) throws RemoteException{} + + @Override + public void onStatus(Status status) throws RemoteException{} + }, req, new IModuleInstallStatusListener.Stub(){ + @Override + public void onModuleInstallStatusUpdate(ModuleInstallStatusUpdate statusUpdate) throws RemoteException{ + if(statusUpdate.installState==ModuleInstallStatusUpdate.STATE_COMPLETED){ + Runnable r=new Runnable(){ + @Override + public void run(){ + if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){ + progress.dismiss(); + startActivityForResult(scannerIntent, SCAN_RESULT); + }else{ + codeContainer.postDelayed(this, 100); + } + } + }; + getActivity().runOnUiThread(r); + GmsClient.disconnectFromService(getActivity(), connectionID); + }else if(statusUpdate.installState==ModuleInstallStatusUpdate.STATE_FAILED || statusUpdate.installState==ModuleInstallStatusUpdate.STATE_CANCELED){ + getActivity().runOnUiThread(()->{ + progress.dismiss(); + Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show(); + }); + GmsClient.disconnectFromService(getActivity(), connectionID); + } + } + }); + }catch(RemoteException e){ + Log.e(TAG, "onSuccess: ", e); + getActivity().runOnUiThread(()->{ + progress.dismiss(); + Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show(); + }); + GmsClient.disconnectFromService(getActivity(), connectionID); + } + } + + @Override + public void onError(Exception error){ + Log.e(TAG, "onError() called with: error = ["+error+"]"); + Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show(); + progress.dismiss(); + } + }); + } + return true; + } + + @Override + protected boolean canGoBack(){ + return true; + } + + @Override + public void onToolbarNavigationClick(){ + dismiss(); + } + + @Override + public boolean wantsCustomNavigationIcon(){ + return true; + } + + @Override + protected int getNavigationIconDrawableResource(){ + return R.drawable.ic_baseline_close_24; + } + + @Override + protected LayoutInflater getToolbarLayoutInflater(){ + return LayoutInflater.from(themeWrapper); + } + + @Override + protected int getToolbarResource(){ + return R.layout.profile_qr_toolbar; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){ + if(requestCode==PERMISSION_RESULT){ + if(grantResults[0]==PackageManager.PERMISSION_GRANTED){ + doSaveCodeAsFile(); + }else if(!getActivity().shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.permission_required) + .setMessage(R.string.storage_permission_to_download) + .setPositiveButton(R.string.open_settings, (dialog, which)->getActivity().startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null)))) + .setNegativeButton(R.string.cancel, null) + .show(); + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data){ + if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){ + Barcode code=BarcodeScanner.getResult(data); + if(code!=null){ + if(code.rawValue.startsWith("https:")){ + ((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID); + dismiss(); + } + } + } + } + + private void dismissWithAnimation(Runnable onDone){ + if(currentTransition!=null) + currentTransition.cancel(); + AnimatorSet set=new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofInt(scrim, "alpha", 0), + ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, V.dp(50)), + ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0), + ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0) + ); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.setDuration(200); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + onDone.run(); + } + }); + currentTransition=set; + set.start(); + } + + private void saveCodeAsFile(){ + if(Build.VERSION.SDK_INT>=29){ + doSaveCodeAsFile(); + }else{ + if(getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){ + requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_RESULT); + }else{ + doSaveCodeAsFile(); + } + } + } + + private void doSaveCodeAsFile(){ + Bitmap bmp=Bitmap.createBitmap(1080, 1080, Bitmap.Config.ARGB_8888); + Canvas c=new Canvas(bmp); + float factor=1080f/codeContainer.getWidth(); + c.scale(factor, factor); + codeContainer.draw(c); + Activity activity=getActivity(); + MastodonAPIController.runInBackground(()->{ + String fileName=account.username+"_"+accountDomain+".png"; + try(OutputStream os=destinationStreamForFile(fileName)){ + bmp.compress(Bitmap.CompressFormat.PNG, 100, os); + activity.runOnUiThread(()->Toast.makeText(activity, R.string.file_saved, Toast.LENGTH_SHORT).show()); + if(Build.VERSION.SDK_INT<29){ + File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName); + MediaScannerConnection.scanFile(activity, new String[]{dstFile.getAbsolutePath()}, new String[]{"image/png"}, null); + } + }catch(IOException x){ + activity.runOnUiThread(()->Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show()); + } + }); + } + + private OutputStream destinationStreamForFile(String fileName) throws IOException{ + if(Build.VERSION.SDK_INT>=29){ + ContentValues values=new ContentValues(); + values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); + values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png"); + ContentResolver cr=getActivity().getContentResolver(); + Uri itemUri=cr.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values); + return cr.openOutputStream(itemUri); + }else{ + return new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName)); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig){ + super.onConfigurationChanged(newConfig); + codeContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ + @Override + public boolean onPreDraw(){ + codeContainer.getViewTreeObserver().removeOnPreDrawListener(this); + updateParticleEmitter(); + return true; + } + }); + } + + private void updateParticleEmitter(){ + int[] loc={0, 0}; + particleAnimContainer.getLocationInWindow(loc); + int x=loc[0], y=loc[1]; + codeContainer.getLocationInWindow(loc); + int cx=loc[0]-x+codeContainer.getWidth()/2; + int cy=loc[1]-y+codeContainer.getHeight()/2; + int r=codeContainer.getWidth()/2-V.dp(10); + particles.setEmitterPosition(cx, cy); + particles.setClipOutBounds(cx-r, cy-r, cx+r, cy+r); + } + + public static class CustomizedLinearLayout extends LinearLayout implements CustomViewHelper{ + public CustomizedLinearLayout(Context context){ + this(context, null); + } + + public CustomizedLinearLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public CustomizedLinearLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + int maxW=dp(400); + FixedAspectRatioFrameLayout aspectLayout=(FixedAspectRatioFrameLayout) getChildAt(0); + if(MeasureSpec.getSize(widthMeasureSpec)>maxW){ + widthMeasureSpec=MeasureSpec.getMode(widthMeasureSpec) | maxW; + aspectLayout.setUseHeight(MeasureSpec.getSize(heightMeasureSpec) CREATOR=new AutoCreator<>(ConnectionResult.class); +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/googleservices/GmsClient.java b/mastodon/src/main/java/org/joinmastodon/android/googleservices/GmsClient.java new file mode 100644 index 0000000000..3b3c5e5a39 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/googleservices/GmsClient.java @@ -0,0 +1,116 @@ +package org.joinmastodon.android.googleservices; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IInterface; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; + +import com.google.android.gms.common.internal.ConnectionInfo; +import com.google.android.gms.common.internal.GetServiceRequest; +import com.google.android.gms.common.internal.IGmsCallbacks; +import com.google.android.gms.common.internal.IGmsServiceBroker; +import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService; + +import java.util.function.Function; + +public class GmsClient{ + private static final String TAG="GmsClient"; + private static final SparseArray currentConnections=new SparseArray<>(); + private static int nextConnectionID=0; + + public static void connectToService(Context context, String action, int id, boolean useDynamicLookup, ServiceConnectionCallback callback, Function asInterface){ + Intent intent; + if(useDynamicLookup){ + try{ + Bundle args=new Bundle(); + args.putString("serviceActionBundleKey", action); + Bundle result=context.getContentResolver().call(new Uri.Builder().scheme("content").authority("com.google.android.gms.chimera").build(), "serviceIntentCall", null, args); + if(result==null) + throw new IllegalStateException("Dynamic lookup failed"); + intent=result.getParcelable("serviceResponseIntentKey"); + if(intent==null) + throw new IllegalStateException("Dynamic lookup returned null"); + }catch(Exception x){ + callback.onError(x); + return; + } + }else{ + intent=new Intent(action); + } + intent.setPackage("com.google.android.gms"); + ServiceConnection conn=new ServiceConnection(){ + @Override + public void onServiceConnected(ComponentName name, IBinder service){ + IGmsServiceBroker broker=IGmsServiceBroker.Stub.asInterface(service); + GetServiceRequest req=new GetServiceRequest(); + req.serviceId=id; + req.packageName=context.getPackageName(); + ServiceConnection serviceConnectionThis=this; + try{ + broker.getService(new IGmsCallbacks.Stub(){ + @Override + public void onPostInitComplete(int statusCode, IBinder binder, Bundle params) throws RemoteException{ + int connectionID=nextConnectionID++; + currentConnections.put(connectionID, serviceConnectionThis); + callback.onSuccess(asInterface.apply(binder), connectionID); + } + + @Override + public void onAccountValidationComplete(int statusCode, Bundle params) throws RemoteException{} + + @Override + public void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, ConnectionInfo info) throws RemoteException{ + onPostInitComplete(statusCode, binder, info!=null ? info.params : null); + } + }, req); + }catch(Exception x){ + callback.onError(x); + context.unbindService(this); + } + } + + @Override + public void onServiceDisconnected(ComponentName name){} + }; + boolean res=context.bindService(intent, conn, Context.BIND_AUTO_CREATE | Context.BIND_DEBUG_UNBIND | Context.BIND_ADJUST_WITH_ACTIVITY); + if(!res){ + context.unbindService(conn); + callback.onError(new IllegalStateException("Service connection failed")); + } + } + + public static void disconnectFromService(Context context, int connectionID){ + ServiceConnection conn=currentConnections.get(connectionID); + if(conn!=null){ + currentConnections.remove(connectionID); + context.unbindService(conn); + } + } + + public static boolean isGooglePlayServicesAvailable(Context context){ + PackageManager pm=context.getPackageManager(); + try{ + pm.getPackageInfo("com.google.android.gms", 0); + return true; + }catch(PackageManager.NameNotFoundException e){ + return false; + } + } + + public static void getModuleInstallerService(Context context, ServiceConnectionCallback callback){ + connectToService(context, "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START", 308, true, callback, IModuleInstallService.Stub::asInterface); + } + + public interface ServiceConnectionCallback{ + void onSuccess(I service, int connectionID); + void onError(Exception error); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/Barcode.java b/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/Barcode.java new file mode 100644 index 0000000000..8cf0ff8415 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/Barcode.java @@ -0,0 +1,253 @@ +package org.joinmastodon.android.googleservices.barcodescanner; + +import android.graphics.Point; + +import org.microg.safeparcel.AutoSafeParcelable; +import org.microg.safeparcel.SafeParceled; + +public class Barcode extends AutoSafeParcelable{ + public static final int FORMAT_UNKNOWN = -1; + public static final int FORMAT_ALL_FORMATS = 0; + public static final int FORMAT_CODE_128 = 1; + public static final int FORMAT_CODE_39 = 2; + public static final int FORMAT_CODE_93 = 4; + public static final int FORMAT_CODABAR = 8; + public static final int FORMAT_DATA_MATRIX = 16; + public static final int FORMAT_EAN_13 = 32; + public static final int FORMAT_EAN_8 = 64; + public static final int FORMAT_ITF = 128; + public static final int FORMAT_QR_CODE = 256; + public static final int FORMAT_UPC_A = 512; + public static final int FORMAT_UPC_E = 1024; + public static final int FORMAT_PDF417 = 2048; + public static final int FORMAT_AZTEC = 4096; + public static final int TYPE_UNKNOWN = 0; + public static final int TYPE_CONTACT_INFO = 1; + public static final int TYPE_EMAIL = 2; + public static final int TYPE_ISBN = 3; + public static final int TYPE_PHONE = 4; + public static final int TYPE_PRODUCT = 5; + public static final int TYPE_SMS = 6; + public static final int TYPE_TEXT = 7; + public static final int TYPE_URL = 8; + public static final int TYPE_WIFI = 9; + public static final int TYPE_GEO = 10; + public static final int TYPE_CALENDAR_EVENT = 11; + public static final int TYPE_DRIVER_LICENSE = 12; + + @SafeParceled(1) + public int format; + @SafeParceled(2) + public String displayValue; + @SafeParceled(3) + public String rawValue; + @SafeParceled(4) + public byte[] rawBytes; + @SafeParceled(5) + public Point[] cornerPoints; + @SafeParceled(6) + public int valueType; + @SafeParceled(7) + public Email emailValue; + @SafeParceled(8) + public Phone phoneValue; + @SafeParceled(9) + public SMS smsValue; + @SafeParceled(10) + public WiFi wifiValue; + @SafeParceled(11) + public UrlBookmark urlBookmarkValue; + @SafeParceled(12) + public GeoPoint geoPointValue; + @SafeParceled(13) + public CalendarEvent calendarEventValue; + @SafeParceled(14) + public ContactInfo contactInfoValue; + @SafeParceled(15) + public DriverLicense driverLicenseValue; + + public static final Creator CREATOR=new AutoCreator<>(Barcode.class); + + // None of the following is needed or used in the Mastodon app and its use cases for QR code scanning, + // but I'm putting it out there in case someone else is crazy enough to want to use Google Services without their libraries + + public static class Email extends AutoSafeParcelable{ + @SafeParceled(1) + public int type; + @SafeParceled(2) + public String address; + @SafeParceled(3) + public String subject; + @SafeParceled(4) + public String body; + + public static final Creator CREATOR=new AutoCreator<>(Email.class); + } + + public static class Phone extends AutoSafeParcelable{ + @SafeParceled(1) + public int type; + @SafeParceled(2) + public String number; + + public static final Creator CREATOR=new AutoCreator<>(Phone.class); + } + + public static class SMS extends AutoSafeParcelable{ + @SafeParceled(1) + public String message; + @SafeParceled(2) + public String phoneNumber; + + public static final Creator CREATOR=new AutoCreator<>(SMS.class); + } + + public static class WiFi extends AutoSafeParcelable{ + @SafeParceled(1) + public String ssid; + @SafeParceled(2) + public String password; + @SafeParceled(3) + public int encryptionType; + + public static final Creator CREATOR=new AutoCreator<>(WiFi.class); + } + + public static class UrlBookmark extends AutoSafeParcelable{ + @SafeParceled(1) + public String title; + @SafeParceled(2) + public String url; + + public static final Creator CREATOR=new AutoCreator<>(UrlBookmark.class); + } + + public static class GeoPoint extends AutoSafeParcelable{ + @SafeParceled(1) + public double lat; + @SafeParceled(2) + public double lng; + + public static final Creator CREATOR=new AutoCreator<>(GeoPoint.class); + } + + public static class EventDateTime extends AutoSafeParcelable{ + @SafeParceled(1) + public int year; + @SafeParceled(2) + public int month; + @SafeParceled(3) + public int day; + @SafeParceled(4) + public int hours; + @SafeParceled(5) + public int minutes; + @SafeParceled(6) + public int seconds; + @SafeParceled(7) + public boolean isUtc; + @SafeParceled(8) + public String rawValue; + + public static final Creator CREATOR=new AutoCreator<>(EventDateTime.class); + } + + public static class CalendarEvent extends AutoSafeParcelable{ + @SafeParceled(1) + public String summary; + @SafeParceled(2) + public String description; + @SafeParceled(3) + public String location; + @SafeParceled(4) + public String organizer; + @SafeParceled(5) + public String status; + @SafeParceled(6) + public EventDateTime start; + @SafeParceled(7) + public EventDateTime end; + + public static final Creator CREATOR=new AutoCreator<>(CalendarEvent.class); + } + + public static class Address extends AutoSafeParcelable{ + @SafeParceled(1) + public int type; + @SafeParceled(2) + public String[] addressLines; + + public static final Creator

CREATOR=new AutoCreator<>(Address.class); + } + + public static class PersonName extends AutoSafeParcelable{ + @SafeParceled(1) + public String formattedName; + @SafeParceled(2) + public String pronunciation; + @SafeParceled(3) + public String prefix; + @SafeParceled(4) + public String first; + @SafeParceled(5) + public String middle; + @SafeParceled(6) + public String last; + @SafeParceled(7) + public String suffix; + + public static final Creator CREATOR=new AutoCreator<>(PersonName.class); + } + + public static class ContactInfo extends AutoSafeParcelable{ + @SafeParceled(1) + public PersonName name; + @SafeParceled(2) + public String organization; + @SafeParceled(3) + public String title; + @SafeParceled(4) + public Phone[] phones; + @SafeParceled(5) + public Email[] emails; + @SafeParceled(6) + public String[] urls; + @SafeParceled(7) + public Address[] addresses; + + public static final Creator CREATOR=new AutoCreator<>(ContactInfo.class); + } + + public static class DriverLicense extends AutoSafeParcelable{ + @SafeParceled(1) + public String documentType; + @SafeParceled(2) + public String firstName; + @SafeParceled(3) + public String middleName; + @SafeParceled(4) + public String lastName; + @SafeParceled(5) + public String gender; + @SafeParceled(6) + public String addressStreet; + @SafeParceled(7) + public String addressCity; + @SafeParceled(8) + public String addressState; + @SafeParceled(9) + public String addressZip; + @SafeParceled(10) + public String licenseNumber; + @SafeParceled(11) + public String issueDate; + @SafeParceled(12) + public String expiryDate; + @SafeParceled(13) + public String birthDate; + @SafeParceled(14) + public String issuingCountry; + + public static final Creator CREATOR=new AutoCreator<>(DriverLicense.class); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/BarcodeScanner.java b/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/BarcodeScanner.java new file mode 100644 index 0000000000..40de1f60f5 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/googleservices/barcodescanner/BarcodeScanner.java @@ -0,0 +1,38 @@ +package org.joinmastodon.android.googleservices.barcodescanner; + +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.os.Parcel; + +import org.joinmastodon.android.MastodonApp; + +public class BarcodeScanner{ + public static Intent createIntent(int formats, boolean allowManualInout, boolean enableAutoZoom){ + Intent intent=new Intent().setPackage("com.google.android.gms").setAction("com.google.android.gms.mlkit.ACTION_SCAN_BARCODE"); + String appName; + ApplicationInfo appInfo=MastodonApp.context.getApplicationInfo(); + if(appInfo.labelRes!=0) + appName=MastodonApp.context.getString(appInfo.labelRes); + else + appName=MastodonApp.context.getPackageManager().getApplicationLabel(appInfo).toString(); + intent.putExtra("extra_calling_app_name", appName); + intent.putExtra("extra_supported_formats", formats); + intent.putExtra("extra_allow_manual_input", allowManualInout); + intent.putExtra("extra_enable_auto_zoom", enableAutoZoom); + return intent; + } + + public static boolean isValidResult(Intent intent){ + return intent!=null && intent.hasExtra("extra_barcode_result"); + } + + public static Barcode getResult(Intent intent){ + byte[] serialized=intent.getByteArrayExtra("extra_barcode_result"); + Parcel parcel=Parcel.obtain(); + parcel.unmarshall(serialized, 0, serialized.length); + parcel.setDataPosition(0); + Barcode barcode=Barcode.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return barcode; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/FancyQrCodeDrawable.java b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/FancyQrCodeDrawable.java new file mode 100644 index 0000000000..18025e327f --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/drawables/FancyQrCodeDrawable.java @@ -0,0 +1,149 @@ +package org.joinmastodon.android.ui.drawables; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +import com.google.zxing.common.BitMatrix; + +import java.util.Arrays; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class FancyQrCodeDrawable extends Drawable{ + private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); + private Path path=new Path(), scaledPath=new Path(); + private int size, logoOffset, logoSize; + private Drawable logo; + + public FancyQrCodeDrawable(BitMatrix code, int color, Drawable logo){ + paint.setColor(color); + this.logo=logo; + size=code.getWidth(); + addMarker(0, 0); + addMarker(size-7, 0); + addMarker(0, size-7); + float[] radii=new float[8]; + logoSize=size/3; + if((size-logoSize)%2!=0){ + logoSize--; + } + logoOffset=(size-logoSize)/2; + for(int y=0;ysize-8 && y<7) || (x<7 && y>size-8)){ + continue; + } + + if(code.get(x, y)){ + boolean t=y>0 && code.get(x, y-1); + boolean b=y0 && code.get(x-1, y); + boolean r=x=3 || (neighborCount==2 && ((l && r) || (t && b)))){ // 3 or 4 neighbors, or part of a straight line + path.addRect(x, y, x+1, y+1, Path.Direction.CW); + continue; + }else if(neighborCount==0){ // No neighbors + path.addCircle(x+0.5f, y+0.5f, 0.5f, Path.Direction.CW); + continue; + } + Arrays.fill(radii, 0); + if(l && t){ // round bottom-right corner + radii[4]=radii[5]=1; + }else if(t && r){ // round bottom-left corner + radii[6]=radii[7]=1; + }else if(r && b){ // round top-left corner + radii[0]=radii[1]=1; + }else if(b && l){ // round top-right corner + radii[2]=radii[3]=1; + }else if(l){ // right side + radii[2]=radii[3]=radii[4]=radii[5]=0.5f; + }else if(t){ // bottom side + radii[4]=radii[5]=radii[6]=radii[7]=0.5f; + }else if(r){ // left side + radii[6]=radii[7]=radii[1]=radii[0]=0.5f; + }else{ // top side + radii[0]=radii[1]=radii[2]=radii[3]=0.5f; + } + path.addRoundRect(x, y, x+1, y+1, radii, Path.Direction.CW); + } + } + } + } + + private void addMarker(int x, int y){ + path.addRoundRect(x, y, x+7, y+7, 2.38f, 2.38f, Path.Direction.CW); + path.addRoundRect(x+1, y+1, x+6, y+6, 1.33f, 1.33f, Path.Direction.CCW); + path.addRoundRect(x+2, y+2, x+5, y+5, 0.8f, 0.8f, Path.Direction.CW); + } + + @Override + public void draw(@NonNull Canvas canvas){ + Rect bounds=getBounds(); + float factor=Math.min(bounds.width(), bounds.height())/(float)size; + float xOff=0, yOff=0; + float bw=bounds.width(), bh=bounds.height(); + if(bw>bh){ + xOff=bw/2f-bh/2f; + }else if(bw activeParticles=new ArrayList<>(), nextActiveParticles=new ArrayList<>(), pool=new ArrayList<>(); + private int emitterX, emitterY; + private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG); + private float[] linearStartColor, linearEndColor; + private long prevFrameTime; + private Random rand=new Random(); + private Rect clipOutBounds=new Rect(); + + public RadialParticleSystemDrawable(long particleLifetime, int birthRate, int startColor, int endColor, float velocity, float velocityVariance, float size){ + this.particleLifetime=particleLifetime; + this.birthRate=birthRate; + this.startColor=startColor; + this.endColor=endColor; + this.velocity=velocity; + this.velocityVariance=velocityVariance; + this.size=size; + + linearStartColor=new float[]{ + ((startColor >> 24) & 0xFF)/255f, + (float)Math.pow(((startColor >> 16) & 0xFF)/255f, 2.2), + (float)Math.pow(((startColor >> 8) & 0xFF)/255f, 2.2), + (float)Math.pow((startColor & 0xFF)/255f, 2.2) + }; + linearEndColor=new float[]{ + ((endColor >> 24) & 0xFF)/255f, + (float)Math.pow(((endColor >> 16) & 0xFF)/255f, 2.2), + (float)Math.pow(((endColor >> 8) & 0xFF)/255f, 2.2), + (float)Math.pow((endColor & 0xFF)/255f, 2.2) + }; + } + + @Override + public void draw(@NonNull Canvas canvas){ + long now=SystemClock.uptimeMillis(); + nextActiveParticles.clear(); + for(Particle p:activeParticles){ + int time=(int)(now-p.birthTime); + if(time>particleLifetime){ + pool.add(p); + continue; + } + nextActiveParticles.add(p); + float x=emitterX+time/1000f*p.velX; + float y=emitterY+time/1000f*p.velY; + if(clipOutBounds.contains((int)x, (int)y)){ + continue; + } + float fraction=time/(float)particleLifetime; + paint.setColor(interpolateColor(fraction)); + canvas.drawCircle(x, y, size, paint); + } + long timeDiff=Math.min(100, now-prevFrameTime); + int newParticleCount=Math.round(timeDiff/1000f*birthRate); + for(int i=0;i tmp=nextActiveParticles; + nextActiveParticles=activeParticles; + activeParticles=tmp; + invalidateSelf(); + prevFrameTime=now; + } + + @Override + public void setAlpha(int alpha){ + + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter){ + + } + + @Override + public int getOpacity(){ + return PixelFormat.TRANSLUCENT; + } + + public void setClipOutBounds(int l, int t, int r, int b){ + clipOutBounds.set(l, t, r, b); + } + + private int interpolateColor(float fraction){ + float a=(linearStartColor[0]+(linearEndColor[0]-linearStartColor[0])*fraction)*255f; + float r=(float)Math.pow(linearStartColor[1]+(linearEndColor[1]-linearStartColor[1])*fraction, 1.0/2.2)*255f; + float g=(float)Math.pow(linearStartColor[2]+(linearEndColor[2]-linearStartColor[2])*fraction, 1.0/2.2)*255f; + float b=(float)Math.pow(linearStartColor[3]+(linearEndColor[3]-linearStartColor[3])*fraction, 1.0/2.2)*255f; + return (Math.round(a) << 24) | (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b); + + } + + public void setEmitterPosition(int x, int y){ + emitterX=x; + emitterY=y; + } + + private static class Particle{ + public long birthTime; + public float velX, velY; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioFrameLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioFrameLayout.java new file mode 100644 index 0000000000..9952947ade --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FixedAspectRatioFrameLayout.java @@ -0,0 +1,57 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import org.joinmastodon.android.R; + +public class FixedAspectRatioFrameLayout extends FrameLayout{ + private float aspectRatio; + private boolean useHeight; + + public FixedAspectRatioFrameLayout(Context context){ + this(context, null); + } + + public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FixedAspectRatioFrameLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FixedAspectRatioImageView); + aspectRatio=ta.getFloat(R.styleable.FixedAspectRatioImageView_aspectRatio, 1); + useHeight=ta.getBoolean(R.styleable.FixedAspectRatioImageView_useHeight, false); + ta.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + if(useHeight){ + int height=MeasureSpec.getSize(heightMeasureSpec); + widthMeasureSpec=Math.round(height*aspectRatio) | MeasureSpec.EXACTLY; + }else{ + int width=MeasureSpec.getSize(widthMeasureSpec); + heightMeasureSpec=Math.round(width/aspectRatio) | MeasureSpec.EXACTLY; + } + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + public float getAspectRatio(){ + return aspectRatio; + } + + public void setAspectRatio(float aspectRatio){ + this.aspectRatio=aspectRatio; + } + + public boolean isUseHeight(){ + return useHeight; + } + + public void setUseHeight(boolean useHeight){ + this.useHeight=useHeight; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/wrapstodon/RoundedImageView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/wrapstodon/RoundedImageView.java new file mode 100644 index 0000000000..1483b6c3df --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/wrapstodon/RoundedImageView.java @@ -0,0 +1,74 @@ +package org.joinmastodon.android.ui.wrapstodon; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Outline; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewOutlineProvider; +import android.widget.ImageView; + +import org.joinmastodon.android.R; + +/** + * Software-rendering-friendly rounded-corners image view. Relies on arcane xrefmode magic. + */ +public class RoundedImageView extends ImageView{ + private int cornerRadius; + private boolean roundBottomCorners=true; + private Paint clearPaint=new Paint(Paint.ANTI_ALIAS_FLAG), paint=new Paint(Paint.ANTI_ALIAS_FLAG); + + public RoundedImageView(Context context){ + this(context, null); + } + + public RoundedImageView(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public RoundedImageView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.RoundedImageView); + cornerRadius=ta.getDimensionPixelOffset(R.styleable.RoundedImageView_cornerRadius, 0); + roundBottomCorners=ta.getBoolean(R.styleable.RoundedImageView_roundBottomCorners, true); + ta.recycle(); + setOutlineProvider(new ViewOutlineProvider(){ + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius); + } + }); + setClipToOutline(true); + clearPaint.setColor(0xFFFFFFFF); + clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + paint.setColor(0xFF0ff000); + } + + public void setCornerRadius(int cornerRadius){ + this.cornerRadius=cornerRadius; + invalidateOutline(); + } + + public void setRoundBottomCorners(boolean roundBottomCorners){ + this.roundBottomCorners=roundBottomCorners; + invalidateOutline(); + } + + @Override + public void draw(Canvas canvas){ + if(canvas.isHardwareAccelerated()){ + super.draw(canvas); + return; + } + canvas.saveLayer(0, 0, getWidth(), getHeight(), null); + canvas.drawRoundRect(0, 0, getWidth(), getHeight()+(roundBottomCorners ? 0 : cornerRadius), cornerRadius, cornerRadius, paint); + canvas.saveLayer(0, 0, getWidth(), getHeight(), clearPaint); + super.draw(canvas); + canvas.restore(); + canvas.restore(); + } +} diff --git a/mastodon/src/main/res/drawable/ic_download_20px.xml b/mastodon/src/main/res/drawable/ic_download_20px.xml new file mode 100644 index 0000000000..7267a460f3 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_download_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_qr_code_20px.xml b/mastodon/src/main/res/drawable/ic_qr_code_20px.xml new file mode 100644 index 0000000000..bff65fba44 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_qr_code_20px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_qr_code_scanner_24px.xml b/mastodon/src/main/res/drawable/ic_qr_code_scanner_24px.xml new file mode 100644 index 0000000000..4b0c8c109a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_qr_code_scanner_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/rect_24dp.xml b/mastodon/src/main/res/drawable/rect_24dp.xml new file mode 100644 index 0000000000..6fbd53fb6c --- /dev/null +++ b/mastodon/src/main/res/drawable/rect_24dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_profile.xml b/mastodon/src/main/res/layout/fragment_profile.xml index 083f129d78..ddbec9569b 100644 --- a/mastodon/src/main/res/layout/fragment_profile.xml +++ b/mastodon/src/main/res/layout/fragment_profile.xml @@ -24,6 +24,7 @@ android:orientation="vertical"> @@ -90,6 +91,20 @@ android:layout_marginTop="16dp" android:layout_marginEnd="4dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +