QR codes for profiles

This commit is contained in:
Grishka 2024-02-22 21:35:46 +03:00
parent b1e999cc9c
commit 5cf222379a
46 changed files with 2086 additions and 6 deletions

View File

@ -9,7 +9,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 84
versionCode 85
versionName "2.3.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
@ -64,6 +64,9 @@ android {
checkReleaseBuilds false
abortOnError false
}
buildFeatures{
aidl true
}
}
dependencies {
@ -81,6 +84,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'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'

View File

@ -31,6 +31,10 @@
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.api;
parcelable Status;

View File

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

View File

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

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.internal;
parcelable GetServiceRequest;

View File

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

View File

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

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleAvailabilityResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallIntentResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallResponse;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallStatusUpdate;

View File

@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall.internal;
parcelable ApiFeatureRequest;

View File

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

View File

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

View File

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

View File

@ -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<Feature> CREATOR=new AutoCreator<>(Feature.class);
}

View File

@ -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<Scope> CREATOR=new AutoCreator<>(Scope.class);
}

View File

@ -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<Status> CREATOR=new AutoCreator<>(Status.class);
@Override
public String toString(){
return "Status{"+
"versionCode="+versionCode+
", statusCode="+statusCode+
", statusMessage='"+statusMessage+'\''+
", pendingIntent="+pendingIntent+
", connectionResult="+connectionResult+
'}';
}
}

View File

@ -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<ConnectionInfo> CREATOR=new AutoCreator<>(ConnectionInfo.class);
}

View File

@ -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<GetServiceRequest> CREATOR=new AutoCreator<>(GetServiceRequest.class);
}

View File

@ -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<ModuleAvailabilityResponse> CREATOR=new AutoCreator<>(ModuleAvailabilityResponse.class);
}

View File

@ -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<ModuleInstallIntentResponse> CREATOR=new AutoCreator<>(ModuleInstallIntentResponse.class);
}

View File

@ -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<ModuleInstallResponse> CREATOR=new AutoCreator<>(ModuleInstallResponse.class);
@Override
public String toString(){
return "ModuleInstallResponse{"+
"sessionID="+sessionID+
", shouldUnregisterListener="+shouldUnregisterListener+
'}';
}
}

View File

@ -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.
* <p>
* 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<ModuleInstallStatusUpdate> CREATOR=new AutoCreator<>(ModuleInstallStatusUpdate.class);
}

View File

@ -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<Feature> features;
@SafeParceled(2)
public boolean urgent;
@SafeParceled(3)
public String sessionId;
@SafeParceled(4)
public String callingPackage;
public static final Creator<ApiFeatureRequest> CREATOR=new AutoCreator<>(ApiFeatureRequest.class);
}

View File

@ -23,8 +23,10 @@ import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -36,6 +38,7 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
@ -129,6 +132,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View tabsDivider;
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private Account account;
private String accountID;
@ -211,6 +215,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabsDivider=content.findViewById(R.id.tabs_divider);
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@ -324,6 +329,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
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;
}
@ -836,17 +849,48 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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);
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);
@ -885,11 +929,40 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
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);
@ -898,6 +971,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
usernameDomain.setVisibility(View.VISIBLE);
qrCodeButton.setVisibility(View.VISIBLE);
bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);

View File

@ -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)<dp(464));
}else{
aspectLayout.setUseHeight(false);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private class AnimatedCornersDrawable extends Drawable{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF tmpRect=new RectF();
public AnimatedCornersDrawable(Context context){
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Primary));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(V.dp(4));
}
@Override
public void draw(@NonNull Canvas canvas){
float inset=V.dp(24);
float radius=V.dp(40);
float animProgress=((float)Math.sin(Math.toRadians(SystemClock.uptimeMillis()/16.6%360.0))+1f)/2f;
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
canvas.save();
float factor=1f+0.025f*animProgress;
paint.setStrokeWidth(V.dp(4)/factor);
canvas.scale(factor, factor, tmpRect.centerX(), tmpRect.centerY());
canvas.drawRoundRect(tmpRect, radius, radius, paint);
canvas.restore();
invalidateSelf();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
float inset=V.dp(24);
float radius=V.dp(40);
float additionalLength=V.dp(40);
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
float[] intervals=new float[]{3.1415f*radius*0.5f+additionalLength*2f, tmpRect.width()-radius*2f-additionalLength*2f};
paint.setPathEffect(new DashPathEffect(intervals, intervals[0]-additionalLength));
updateParticleEmitter();
}
}
}

View File

@ -151,7 +151,7 @@ public class ThreadFragment extends StatusListFragment{
replyButton.setOnClickListener(v->openReply());
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
}
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent();

View File

@ -0,0 +1,47 @@
package org.joinmastodon.android.googleservices;
import android.app.PendingIntent;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ConnectionResult extends AutoSafeParcelable{
public static final int UNKNOWN = -1;
public static final int SUCCESS = 0;
public static final int SERVICE_MISSING = 1;
public static final int SERVICE_VERSION_UPDATE_REQUIRED = 2;
public static final int SERVICE_DISABLED = 3;
public static final int SIGN_IN_REQUIRED = 4;
public static final int INVALID_ACCOUNT = 5;
public static final int RESOLUTION_REQUIRED = 6;
public static final int NETWORK_ERROR = 7;
public static final int INTERNAL_ERROR = 8;
public static final int SERVICE_INVALID = 9;
public static final int DEVELOPER_ERROR = 10;
public static final int LICENSE_CHECK_FAILED = 11;
public static final int CANCELED = 13;
public static final int TIMEOUT = 14;
public static final int INTERRUPTED = 15;
public static final int API_UNAVAILABLE = 16;
public static final int SIGN_IN_FAILED = 17;
public static final int SERVICE_UPDATING = 18;
public static final int SERVICE_MISSING_PERMISSION = 19;
public static final int RESTRICTED_PROFILE = 20;
public static final int RESOLUTION_ACTIVITY_NOT_FOUND = 22;
public static final int API_DISABLED = 23;
public static final int API_DISABLED_FOR_CONNECTION = 24;
@Deprecated
public static final int DRIVE_EXTERNAL_STORAGE_REQUIRED = 1500;
@SafeParceled(1)
public int versionCode;
@SafeParceled(2)
public int errorCode;
@SafeParceled(3)
public PendingIntent resolution;
@SafeParceled(4)
public String errorMessage;
public static final Creator<ConnectionResult> CREATOR=new AutoCreator<>(ConnectionResult.class);
}

View File

@ -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<ServiceConnection> currentConnections=new SparseArray<>();
private static int nextConnectionID=0;
public static <I extends IInterface> void connectToService(Context context, String action, int id, boolean useDynamicLookup, ServiceConnectionCallback<I> callback, Function<IBinder, I> 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<IModuleInstallService> callback){
connectToService(context, "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START", 308, true, callback, IModuleInstallService.Stub::asInterface);
}
public interface ServiceConnectionCallback<I extends IInterface>{
void onSuccess(I service, int connectionID);
void onError(Exception error);
}
}

View File

@ -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<Barcode> 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<Email> 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<Phone> 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<SMS> 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<WiFi> 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<UrlBookmark> 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<GeoPoint> 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<EventDateTime> 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<CalendarEvent> 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<Address> 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<PersonName> 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<ContactInfo> 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<DriverLicense> CREATOR=new AutoCreator<>(DriverLicense.class);
}
}

View File

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

View File

@ -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;y<logoSize;y++){
for(int x=0;x<logoSize;x++){
code.unset(x+logoOffset, y+logoOffset);
}
}
for(int y=0;y<size;y++){
for(int x=0;x<size;x++){
// Skip corner markers because they turn out ugly with this algorithm
if((x<7 && y<7) || (x>size-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=y<size-1 && code.get(x, y+1);
boolean l=x>0 && code.get(x-1, y);
boolean r=x<size-1 && code.get(x+1, y);
int neighborCount=(l ? 1 : 0)+(t ? 1 : 0)+(r ? 1 : 0)+(b ? 1 : 0);
// Special-case optimizations
if(neighborCount>=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<bh){
yOff=bh/2f-bw/2f;
}
canvas.save();
canvas.translate(-bounds.left+xOff, -bounds.top+yOff);
canvas.drawPath(scaledPath, paint);
int scaledOffset=Math.round((logoOffset+1)*factor);
int scaledSize=Math.round((logoSize-2)*factor);
logo.setBounds(scaledOffset, scaledOffset, scaledOffset+scaledSize, scaledOffset+scaledSize);
logo.draw(canvas);
canvas.restore();
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth(){
return size;
}
@Override
public int getIntrinsicHeight(){
return size;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
float factor=Math.min(bounds.width(), bounds.height())/(float)size;
scaledPath.rewind();
Matrix matrix=new Matrix();
matrix.setScale(factor, factor);
scaledPath.addPath(path, matrix);
}
}

View File

@ -0,0 +1,133 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import java.util.ArrayList;
import java.util.Random;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class RadialParticleSystemDrawable extends Drawable{
private long particleLifetime;
private int birthRate;
private int startColor, endColor;
private float velocity, velocityVariance;
private float size;
private ArrayList<Particle> 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<newParticleCount;i++){
Particle p;
if(!pool.isEmpty())
p=pool.remove(pool.size()-1);
else
p=new Particle();
p.birthTime=now;
double angle=rand.nextDouble()*Math.PI*2;
float vel=velocity+velocityVariance*(rand.nextFloat()*2-1f);
p.velX=vel*(float)Math.cos(angle);
p.velY=vel*(float)Math.sin(angle);
nextActiveParticles.add(p);
}
ArrayList<Particle> 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;
}
}

View File

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

View File

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

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M5.5,16Q4.875,16 4.438,15.562Q4,15.125 4,14.5V13H5.5V14.5Q5.5,14.5 5.5,14.5Q5.5,14.5 5.5,14.5H14.5Q14.5,14.5 14.5,14.5Q14.5,14.5 14.5,14.5V13H16V14.5Q16,15.125 15.562,15.562Q15.125,16 14.5,16ZM10,13 L6,9 7.062,7.938 9.25,10.125V3H10.75V10.125L12.938,7.938L14,9Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M2.5,9.125V2.5H9.125V9.125ZM4.25,7.375H7.375V4.25H4.25ZM2.5,17.5V10.875H9.125V17.5ZM4.25,15.75H7.417V12.625H4.25ZM10.875,9.125V2.5H17.5V9.125ZM12.625,7.375H15.75V4.25H12.625ZM15.854,17.5V15.833H17.5V17.5ZM10.875,12.521V10.875H12.542V12.521ZM12.542,14.167V12.521H14.208V14.167ZM10.875,15.833V14.167H12.542V15.833ZM12.542,17.5V15.833H14.208V17.5ZM14.208,15.833V14.167H15.854V15.833ZM14.208,12.521V10.875H15.854V12.521ZM15.854,14.167V12.521H17.5V14.167Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M2,7V2H7V4H4V7ZM2,22V17H4V20H7V22ZM17,22V20H20V17H22V22ZM20,7V4H17V2H22V7ZM17.5,17.5H19V19H17.5ZM17.5,14.5H19V16H17.5ZM16,16H17.5V17.5H16ZM14.5,17.5H16V19H14.5ZM13,16H14.5V17.5H13ZM16,13H17.5V14.5H16ZM14.5,14.5H16V16H14.5ZM13,13H14.5V14.5H13ZM19,5V11H13V5ZM11,13V19H5V13ZM11,5V11H5V5ZM9.5,17.5V14.5H6.5V17.5ZM9.5,9.5V6.5H6.5V9.5ZM17.5,9.5V6.5H14.5V9.5Z"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="24dp"/>
<solid android:color="#000"/>
</shape>

View File

@ -24,6 +24,7 @@
android:orientation="vertical">
<RelativeLayout
android:id="@+id/profile_header"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -291,8 +292,9 @@
<FrameLayout
android:id="@+id/profile_action_btn_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<org.joinmastodon.android.ui.views.ProgressBarButton
android:id="@+id/profile_action_btn"
@ -314,6 +316,20 @@
android:outlineProvider="none"
android:visibility="gone" />
</FrameLayout>
<ImageButton
android:id="@+id/qr_code"
android:layout_width="36.67dp"
android:layout_height="36.67dp"
android:layout_gravity="center_vertical"
style="@style/Widget.Mastodon.M3.Button.Outlined"
android:tint="?colorM3OnSurfaceVariant"
android:layout_marginStart="8dp"
android:layout_marginEnd="1.67dp"
android:contentDescription="@string/qr_code"
android:scaleType="centerCrop"
android:padding="9dp"
android:src="@drawable/ic_qr_code_20px"/>
</LinearLayout>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<include layout="@layout/profile_qr_toolbar" />
<view class="org.joinmastodon.android.fragments.ProfileQrCodeFragment$CustomizedLinearLayout"
android:id="@+id/particle_animation_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:orientation="vertical">
<org.joinmastodon.android.ui.views.FixedAspectRatioFrameLayout
android:id="@+id/corner_animation_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:aspectRatio="1">
<LinearLayout
android:id="@+id/code_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="40dp"
android:orientation="vertical"
android:paddingTop="17dp"
android:background="@drawable/rect_24dp"
android:backgroundTint="?colorM3Primary">
<View
android:id="@+id/code"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="12dp"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="16dp"
android:baselineAligned="false"
android:orientation="horizontal">
<me.grishka.appkit.views.RoundedImageView
android:id="@+id/avatar"
android:layout_width="20dp"
android:layout_height="20dp"
app:cornerRadius="10dp"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_marginHorizontal="4dp"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnPrimary"
android:alpha="0.7"
tools:text="Gargron"/>
<TextView
android:id="@+id/domain"
android:layout_width="wrap_content"
android:layout_height="20dp"
android:alpha="0.7"
android:textAppearance="@style/m3_label_small"
android:textColor="?colorM3Primary"
android:paddingHorizontal="4dp"
android:background="@drawable/rect_4dp"
android:backgroundTint="?colorM3OnPrimary"
android:gravity="center_vertical"
android:singleLine="true"
android:ellipsize="end"
tools:text="mastodon.social"/>
</LinearLayout>
</LinearLayout>
</org.joinmastodon.android.ui.views.FixedAspectRatioFrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:paddingHorizontal="16dp"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/share_btn"
android:layout_width="0dp"
android:layout_height="40dp"
android:layout_weight="1"
style="@style/Widget.Mastodon.M3.Button.Filled">
<TextView
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:drawableStart="@drawable/ic_share_20px"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:background="@null"
android:padding="0dp"
android:drawablePadding="7dp"
android:drawableTint="?colorM3OnPrimary"
android:clickable="false"
android:focusable="false"
android:text="@string/share_user"/>
</FrameLayout>
<Button
android:id="@+id/save_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:drawableStart="@drawable/ic_download_20px"
android:drawablePadding="7dp"
android:drawableTint="?colorM3OnPrimary"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/save"/>
</LinearLayout>
</view>
</LinearLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
tools:showIn="@layout/fragment_profile_qr" />

View File

@ -63,4 +63,9 @@
<declare-styleable name="ProgressBarButton">
<attr name="progressBar" format="reference"/>
</declare-styleable>
<declare-styleable name="RoundedImageView">
<attr name="cornerRadius" format="dimension"/>
<attr name="roundBottomCorners" format="boolean"/>
</declare-styleable>
</resources>

View File

@ -702,4 +702,6 @@
<string name="what_is_activitypub_title">Whats ActivityPub?</string>
<string name="what_is_activitypub">ActivityPub is like the language Mastodon speaks with other social networks.\n\nIt lets you connect and interact with people not just on Mastodon, but across different social apps too.</string>
<string name="handle_copied">Handle copied to clipboard.</string>
<string name="qr_code">QR code</string>
<string name="scan_qr_code">Scan QR code</string>
</resources>