From 61c5fc10572aa117fa0cd77cb6fba1ac86b62130 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 4 Jan 2021 18:39:48 -0500 Subject: [PATCH] Add shake-to-report for internal users. --- .../securesms/ApplicationContext.java | 5 + .../thoughtcrime/securesms/BaseActivity.java | 2 + .../dependencies/ApplicationDependencies.java | 15 ++ .../ApplicationDependencyProvider.java | 6 + .../securesms/shakereport/ShakeToReport.java | 138 ++++++++++ .../securesms/sharing/ShareIntents.java | 4 +- app/src/main/res/values/strings.xml | 8 + .../org/signal/core/util/ShakeDetector.java | 238 ++++++++++++++++++ 8 files changed, 414 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java create mode 100644 core-util/src/main/java/org/signal/core/util/ShakeDetector.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index e2974ea3a..b5804463f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -17,6 +17,7 @@ package org.thoughtcrime.securesms; import android.content.Context; +import android.hardware.SensorManager; import android.os.Build; import androidx.annotation.NonNull; @@ -32,6 +33,7 @@ import com.google.android.gms.security.ProviderInstaller; import org.conscrypt.Conscrypt; import org.signal.aesgcmprovider.AesGcmProvider; +import org.signal.core.util.ShakeDetector; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.AndroidLogger; import org.signal.core.util.logging.Log; @@ -69,6 +71,7 @@ import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; +import org.thoughtcrime.securesms.shakereport.ShakeToReport; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -177,6 +180,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi executePendingContactSync(); KeyCachingService.onAppForegrounded(this); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); + ApplicationDependencies.getShakeToReport().enable(); checkBuildExpiration(); }); @@ -190,6 +194,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi KeyCachingService.onAppBackgrounded(this); ApplicationDependencies.getMessageNotifier().clearVisibleThread(); ApplicationDependencies.getFrameRateTracker().end(); + ApplicationDependencies.getShakeToReport().disable(); } public ExpiringMessageManager getExpiringMessageManager() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java index 686ed159a..4227e19e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActivity.java @@ -15,6 +15,7 @@ import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityOptionsCompat; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.util.ConfigurationUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; @@ -44,6 +45,7 @@ public abstract class BaseActivity extends AppCompatActivity { @Override protected void onStart() { logEvent("onStart()"); + ApplicationDependencies.getShakeToReport().registerActivity(this); super.onStart(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 2ee8306bb..0c52c1a00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; +import org.thoughtcrime.securesms.shakereport.ShakeToReport; import org.thoughtcrime.securesms.util.EarlyMessageCache; import org.thoughtcrime.securesms.util.FrameRateTracker; import org.thoughtcrime.securesms.util.Hex; @@ -69,6 +70,7 @@ public class ApplicationDependencies { private static volatile TypingStatusSender typingStatusSender; private static volatile DatabaseObserver databaseObserver; private static volatile TrimThreadsByDateManager trimThreadsByDateManager; + private static volatile ShakeToReport shakeToReport; @MainThread public static void init(@NonNull Application application, @NonNull Provider provider) { @@ -321,6 +323,18 @@ public class ApplicationDependencies { return databaseObserver; } + public static @NonNull ShakeToReport getShakeToReport() { + if (shakeToReport == null) { + synchronized (LOCK) { + if (shakeToReport == null) { + shakeToReport = provider.provideShakeToReport(); + } + } + } + + return shakeToReport; + } + public interface Provider { @NonNull GroupsV2Operations provideGroupsV2Operations(); @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(); @@ -340,5 +354,6 @@ public class ApplicationDependencies { @NonNull TypingStatusRepository provideTypingStatusRepository(); @NonNull TypingStatusSender provideTypingStatusSender(); @NonNull DatabaseObserver provideDatabaseObserver(); + @NonNull ShakeToReport provideShakeToReport(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index f489b3b71..61ede7804 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; +import org.thoughtcrime.securesms.shakereport.ShakeToReport; import org.thoughtcrime.securesms.util.AlarmSleepTimer; import org.thoughtcrime.securesms.util.ByteUnit; import org.thoughtcrime.securesms.util.EarlyMessageCache; @@ -201,6 +202,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return new DatabaseObserver(context); } + @Override + public @NonNull ShakeToReport provideShakeToReport() { + return new ShakeToReport(context); + } + private static class DynamicCredentialsProvider implements CredentialsProvider { private final Context context; diff --git a/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java b/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java new file mode 100644 index 000000000..bff7ecf6f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java @@ -0,0 +1,138 @@ +package org.thoughtcrime.securesms.shakereport; + +import android.app.Activity; +import android.app.Application; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import org.signal.core.util.ShakeDetector; +import org.signal.core.util.logging.Log; +import org.signal.core.util.tracing.Tracer; +import org.thoughtcrime.securesms.ApplicationContext; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository; +import org.thoughtcrime.securesms.sharing.ShareIntents; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.ServiceUtil; +import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; + +import java.lang.ref.WeakReference; + +/** + * A class that will detect a shake and then prompts the user to submit a debuglog. Basically a + * shortcut to submit a debuglog from anywhere. + */ +public final class ShakeToReport implements ShakeDetector.Listener { + + private static final String TAG = Log.tag(ShakeToReport.class); + + private final Application application; + private final ShakeDetector detector; + + private WeakReference weakActivity; + + public ShakeToReport(@NonNull Application application) { + this.application = application; + this.detector = new ShakeDetector(this); + this.weakActivity = new WeakReference<>(null); + } + + public void enable() { + if (!FeatureFlags.internalUser()) return; + + detector.start(ServiceUtil.getSensorManager(application)); + } + + public void disable() { + if (!FeatureFlags.internalUser()) return; + + detector.stop(); + } + + public void registerActivity(@NonNull Activity activity) { + if (!FeatureFlags.internalUser()) return; + + this.weakActivity = new WeakReference<>(activity); + } + + @Override + public void onShakeDetected() { + Activity activity = weakActivity.get(); + if (activity == null) { + Log.w(TAG, "No registered activity!"); + return; + } + + disable(); + + new AlertDialog.Builder(activity) + .setTitle(R.string.ShakeToReport_shake_detected) + .setMessage(R.string.ShakeToReport_submit_debug_log) + .setNegativeButton(android.R.string.cancel, (d, i) -> { + d.dismiss(); + enableIfVisible(); + }) + .setPositiveButton(R.string.ShakeToReport_submit, (d, i) -> { + d.dismiss(); + submitLog(activity); + }) + .show(); + } + + private void submitLog(@NonNull Activity activity) { + AlertDialog spinner = SimpleProgressDialog.show(activity); + SubmitDebugLogRepository repo = new SubmitDebugLogRepository(); + + Log.i(TAG, "Submitting log..."); + + repo.getLogLines(lines -> { + Log.i(TAG, "Retrieved log lines..."); + + repo.submitLog(lines, Tracer.getInstance().serialize(), url -> { + Log.i(TAG, "Logs uploaded!"); + + Util.runOnMain(() -> { + spinner.dismiss(); + + if (url.isPresent()) { + showPostSubmitDialog(activity, url.get()); + } else { + Toast.makeText(activity, R.string.ShakeToReport_failed_to_submit, Toast.LENGTH_SHORT).show(); + enableIfVisible(); + } + }); + }); + }); + } + + private void showPostSubmitDialog(@NonNull Activity activity, @NonNull String url) { + AlertDialog dialog = new AlertDialog.Builder(activity) + .setTitle(R.string.ShakeToReport_success) + .setMessage(url) + .setNegativeButton(android.R.string.cancel, (d, i) -> { + d.dismiss(); + enableIfVisible(); + }) + .setPositiveButton(R.string.ShakeToReport_share, (d, i) -> { + d.dismiss(); + enableIfVisible(); + + activity.startActivity(new ShareIntents.Builder(activity) + .setText(url) + .build()); + }) + .show(); + + ((TextView) dialog.findViewById(android.R.id.message)).setTextIsSelectable(true); + } + + private void enableIfVisible() { + if (ApplicationContext.getInstance(application).isAppVisible()) { + enable(); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareIntents.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareIntents.java index fb674651b..d2e69e7e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareIntents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/ShareIntents.java @@ -41,8 +41,8 @@ public final class ShareIntents { @Nullable StickerLocator extraSticker, boolean isBorderless) { - this.extraText = extraText; - this.extraMedia = extraMedia; + this.extraText = extraText; + this.extraMedia = extraMedia; this.extraSticker = extraSticker; this.isBorderless = isBorderless; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 22e703393..bcfa3bf04 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1518,6 +1518,14 @@ Contacts Messages + + Shake detected + Submit debug log? + Submit + Failed to submit :( + Success! + Share + Add to Contacts Invite to Signal diff --git a/core-util/src/main/java/org/signal/core/util/ShakeDetector.java b/core-util/src/main/java/org/signal/core/util/ShakeDetector.java new file mode 100644 index 000000000..6cbcd7f36 --- /dev/null +++ b/core-util/src/main/java/org/signal/core/util/ShakeDetector.java @@ -0,0 +1,238 @@ +// Copyright 2010 Square, Inc. +// Modified 2020 Signal + +package org.signal.core.util; + +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +/** + * Detects phone shaking. If more than 75% of the samples taken in the past 0.5s are + * accelerating, the device is a) shaking, or b) free falling 1.84m (h = + * 1/2*g*t^2*3/4). + * + * @author Bob Lee (bob@squareup.com) + * @author Eric Burke (eric@squareup.com) + */ +public class ShakeDetector implements SensorEventListener { + + private static final int SHAKE_THRESHOLD = 13; + + /** Listens for shakes. */ + public interface Listener { + /** Called on the main thread when the device is shaken. */ + void onShakeDetected(); + } + + private final SampleQueue queue = new SampleQueue(); + private final Listener listener; + + private SensorManager sensorManager; + private Sensor accelerometer; + + public ShakeDetector(Listener listener) { + this.listener = listener; + } + + /** + * Starts listening for shakes on devices with appropriate hardware. + * + * @return true if the device supports shake detection. + */ + public boolean start(SensorManager sensorManager) { + if (accelerometer != null) { + return true; + } + + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + + if (accelerometer != null) { + this.sensorManager = sensorManager; + sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_FASTEST); + } + + return accelerometer != null; + } + + /** + * Stops listening. Safe to call when already stopped. Ignored on devices without appropriate + * hardware. + */ + public void stop() { + if (accelerometer != null) { + queue.clear(); + sensorManager.unregisterListener(this, accelerometer); + sensorManager = null; + accelerometer = null; + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + boolean accelerating = isAccelerating(event); + long timestamp = event.timestamp; + + queue.add(timestamp, accelerating); + + if (queue.isShaking()) { + queue.clear(); + listener.onShakeDetected(); + } + } + + /** Returns true if the device is currently accelerating. */ + private boolean isAccelerating(SensorEvent event) { + float ax = event.values[0]; + float ay = event.values[1]; + float az = event.values[2]; + + // Instead of comparing magnitude to ACCELERATION_THRESHOLD, + // compare their squares. This is equivalent and doesn't need the + // actual magnitude, which would be computed using (expensive) Math.sqrt(). + final double magnitudeSquared = ax * ax + ay * ay + az * az; + + return magnitudeSquared > SHAKE_THRESHOLD * SHAKE_THRESHOLD; + } + + /** Queue of samples. Keeps a running average. */ + static class SampleQueue { + + /** Window size in ns. Used to compute the average. */ + private static final long MAX_WINDOW_SIZE = 500000000; // 0.5s + private static final long MIN_WINDOW_SIZE = MAX_WINDOW_SIZE >> 1; // 0.25s + + /** + * Ensure the queue size never falls below this size, even if the device + * fails to deliver this many events during the time window. The LG Ally + * is one such device. + */ + private static final int MIN_QUEUE_SIZE = 4; + + private final SamplePool pool = new SamplePool(); + + private Sample oldest; + private Sample newest; + private int sampleCount; + private int acceleratingCount; + + /** + * Adds a sample. + * + * @param timestamp in nanoseconds of sample + * @param accelerating true if > {@link #SHAKE_THRESHOLD}. + */ + void add(long timestamp, boolean accelerating) { + purge(timestamp - MAX_WINDOW_SIZE); + + Sample added = pool.acquire(); + + added.timestamp = timestamp; + added.accelerating = accelerating; + added.next = null; + + if (newest != null) { + newest.next = added; + } + + newest = added; + + if (oldest == null) { + oldest = added; + } + + sampleCount++; + + if (accelerating) { + acceleratingCount++; + } + } + + /** Removes all samples from this queue. */ + void clear() { + while (oldest != null) { + Sample removed = oldest; + oldest = removed.next; + pool.release(removed); + } + + newest = null; + sampleCount = 0; + acceleratingCount = 0; + } + + /** Purges samples with timestamps older than cutoff. */ + void purge(long cutoff) { + while (sampleCount >= MIN_QUEUE_SIZE && oldest != null && cutoff - oldest.timestamp > 0) { + Sample removed = oldest; + + if (removed.accelerating) { + acceleratingCount--; + } + + sampleCount--; + + oldest = removed.next; + + if (oldest == null) { + newest = null; + } + + pool.release(removed); + } + } + + /** + * Returns true if we have enough samples and more than 3/4 of those samples + * are accelerating. + */ + boolean isShaking() { + return newest != null && + oldest != null && + newest.timestamp - oldest.timestamp >= MIN_WINDOW_SIZE && + acceleratingCount >= (sampleCount >> 1) + (sampleCount >> 2); + } + } + + /** An accelerometer sample. */ + static class Sample { + /** Time sample was taken. */ + long timestamp; + + /** If acceleration > {@link #SHAKE_THRESHOLD}. */ + boolean accelerating; + + /** Next sample in the queue or pool. */ + Sample next; + } + + /** Pools samples. Avoids garbage collection. */ + static class SamplePool { + private Sample head; + + /** Acquires a sample from the pool. */ + Sample acquire() { + Sample acquired = head; + + if (acquired == null) { + acquired = new Sample(); + } else { + head = acquired.next; + } + + return acquired; + } + + /** Returns a sample to the pool. */ + void release(Sample sample) { + sample.next = head; + head = sample; + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } +} +