blob: 12fe964c5266b26f8c9d98c718aa5b06f933663f [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package io.v.android.debug;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.annotation.StringRes;
import android.widget.EditText;
import org.joda.time.Duration;
import org.joda.time.ReadableDuration;
import io.v.android.inspectors.RemoteInspectors;
import io.v.android.VAndroidContext;
import io.v.v23.android.R;
import io.v.v23.context.VContext;
import io.v.v23.verror.VException;
import lombok.Getter;
/**
* Encapsulates the remote inspection debug feature. This is a utility wrapper around
* {@link RemoteInspectors}.
* <p>
* The UI for this feature includes a dialog prompting the user for an e-mail address to which it
* will send an e-mail detailing how the recipient can inspect the state of the application. This
* may be useful for live debugging (the recipient can inspect logs, exported stats,
* system information, etc.).
* <p>
* The dialog also allows the user to disable remote inspection, thereby refusing requests from
* remote inspectors even if they have permissions, or enable remote inspection, allowing anyone
* with pre-existing permissions to inspect remotely.
* <p>
* This class also manages a shared preference to persist remote inspectability across app sessions.
* This preference is stored as a Boolean preference named
* <code>{@value #REMOTE_INSPECTION_PREF}</code>. If this setting is already set upon instantiation,
* remote inspection is immediately enabled.
*/
public class RemoteInspection {
public static final String REMOTE_INSPECTION_PREF = "remoteInspection";
public static final Duration DEFAULT_INVITATION_DURATION = Duration.standardDays(1);
public static final String EMAIL_MIME_TYPE = "message/rfc822";
@Getter
private final VAndroidContext<?> mVAndroidContext;
private final SharedPreferences mSharedPreferences;
private VContext mVContext;
private RemoteInspectors mRemoteInspectors;
public RemoteInspection(final VAndroidContext<?> context,
final SharedPreferences sharedPreferences) {
mVAndroidContext = context;
mSharedPreferences = sharedPreferences;
if (mSharedPreferences.getBoolean(REMOTE_INSPECTION_PREF, false)) {
enable();
}
}
public boolean isEnabled() {
return mRemoteInspectors != null;
}
private void onError(final @StringRes int message, final Throwable t) {
mVAndroidContext.getErrorReporter().onError(message, t);
}
/**
* Starts the remote inspection server. This allows any user with a remote inspection blessing
* to access debug information through a browser.
*
* @see #disable()
*/
public void enable() {
if (mRemoteInspectors == null) {
mVContext = mVAndroidContext.getVContext().withCancel();
try {
mRemoteInspectors = new RemoteInspectors(mVContext);
} catch (final VException e) {
onError(R.string.err_inspect_setup, e);
}
mSharedPreferences.edit()
.putBoolean(REMOTE_INSPECTION_PREF, true)
.apply();
}
}
/**
* Stops the remote inspection server. Users with remote inspection blessings will not be able
* to access live debug information until remote inspection is re-enabled.
*
* @see #enable()
*/
public void disable() {
if (mRemoteInspectors != null) {
mVContext.cancel();
mRemoteInspectors = null;
mSharedPreferences.edit()
.putBoolean(REMOTE_INSPECTION_PREF, false)
.apply();
}
}
/**
* Invite a remote user to inspect state of the application. This method enables remote
* inspection if not already enabled.
*
* @see RemoteInspectors#invite(String, ReadableDuration)
*
* @param invitee the name to refer to the remote user as (typically an email address).
* @param duration time after which inspection privileges expire.
*
* @return a textual description of how the remote user can access state of the running
* application.
*/
public String mintInvitation(final String invitee, final ReadableDuration duration)
throws VException {
// TODO(rosswang): Should we provide an overload that skips the enable() call?
enable();
return mRemoteInspectors.invite(invitee, duration);
}
/**
* Sends a freshly minted invitation to the given e-mail address. If there is no e-mail client
* installed, an error is reported to the user.
*/
public void emailInvitation(final String email, final ReadableDuration duration) {
final String content;
try {
content = mintInvitation(email, duration);
} catch (final VException e) {
onError(R.string.err_inspect_invite, e);
return;
}
final Context context = mVAndroidContext.getAndroidContext();
final Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType(EMAIL_MIME_TYPE);
intent.putExtra(Intent.EXTRA_EMAIL, new String[]{email});
intent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.inspect_email_subject));
intent.putExtra(Intent.EXTRA_TEXT, content);
if (intent.resolveActivity(context.getPackageManager()) != null) {
context.startActivity(intent);
} else {
onError(R.string.err_inspect_email, null);
}
}
/**
* Shows a dialog box to send remote inspection invitations, enable remote inspection, and
* disable remote inspection.
*/
public void showDialog() {
final AlertDialog.Builder builder =
new AlertDialog.Builder(mVAndroidContext.getAndroidContext())
.setView(R.layout.dialog_inspect)
.setCancelable(true)
.setPositiveButton(R.string.inspect_invite, (d, b) -> {
final EditText email = (EditText)
((AlertDialog) d).findViewById(R.id.inspect_email);
emailInvitation(email.getText().toString(),
DEFAULT_INVITATION_DURATION);
});
if (isEnabled()) {
builder.setNegativeButton(R.string.inspect_disable, (d, b) -> disable());
} else {
builder.setNegativeButton(R.string.inspect_enable, (d, b) -> enable());
}
builder.show();
}
}