610 lines
22 KiB
Java
610 lines
22 KiB
Java
package backone.cloud.call;
|
|
|
|
import android.app.Service;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.content.ServiceConnection;
|
|
import android.content.res.Resources;
|
|
import android.media.AudioManager;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import androidx.annotation.NonNull;
|
|
|
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
|
|
|
import androidx.appcompat.app.AlertDialog;
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
|
|
|
import android.os.Looper;
|
|
import android.view.Menu;
|
|
import android.view.MenuItem;
|
|
import android.view.View;
|
|
import android.view.animation.AlphaAnimation;
|
|
import android.view.animation.AnimationSet;
|
|
import android.view.animation.TranslateAnimation;
|
|
import android.widget.AdapterView;
|
|
import android.widget.EditText;
|
|
import android.widget.ListView;
|
|
import android.widget.PopupMenu;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import org.greenrobot.eventbus.EventBus;
|
|
import org.greenrobot.eventbus.Subscribe;
|
|
import org.greenrobot.eventbus.ThreadMode;
|
|
import org.json.JSONException;
|
|
|
|
import android.net.VpnService;
|
|
|
|
import cloud.backone.backoneconnect.BackOneConnectApplication;
|
|
import cloud.backone.backoneconnect.events.NodeStatusEvent;
|
|
import cloud.backone.backoneconnect.events.RequestNodeStatusEvent;
|
|
import cloud.backone.backoneconnect.model.Network;
|
|
import cloud.backone.backoneconnect.model.NetworkDao;
|
|
import cloud.backone.backoneconnect.service.ZeroTierOneService;
|
|
import com.zerotier.sdk.NodeStatus;
|
|
|
|
import java.util.List;
|
|
|
|
|
|
// the main view with tabs
|
|
public class MainActivity extends MeshengerActivity implements ServiceConnection, AdapterView.OnItemClickListener {
|
|
private static final int MIC_PERMISSION_REQUEST_CODE = 2;
|
|
private static final long STATUS_REFRESH_INTERVAL_MS = 30_000L;
|
|
/** Ignore brief ZeroTier EVENT_OFFLINE flaps while the TUN interface is still up. */
|
|
private static final long VPN_OFFLINE_DEBOUNCE_MS = 8_000L;
|
|
|
|
public MainService.MainBinder binder;
|
|
private ListView contactListView;
|
|
private TextView ownStatusText;
|
|
private boolean fabExpanded = false;
|
|
private FloatingActionButton fabScan;
|
|
private FloatingActionButton fabGen;
|
|
private FloatingActionButton fab;
|
|
SwipeRefreshLayout swipeRefreshLayout;
|
|
|
|
private boolean serviceBound = false;
|
|
private boolean activityResumed = false;
|
|
private boolean vpnOnline = false;
|
|
private final EventBus eventBus = EventBus.getDefault();
|
|
private final Handler statusRefreshHandler = new Handler(Looper.getMainLooper());
|
|
private final Runnable vpnOfflineRunnable = () -> {
|
|
// Only show offline after debounce if the TUN interface is really gone.
|
|
if (Utils.getFirstTunIpv4() == null) {
|
|
vpnOnline = false;
|
|
updateOwnStatusText();
|
|
} else {
|
|
vpnOnline = true;
|
|
updateOwnStatusText();
|
|
}
|
|
};
|
|
private final Runnable statusRefreshRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
refreshContactStatuses();
|
|
refreshVpnFromTunnel();
|
|
statusRefreshHandler.postDelayed(this, STATUS_REFRESH_INTERVAL_MS);
|
|
}
|
|
};
|
|
|
|
@Override
|
|
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
|
|
log("onItemClick");
|
|
if (binder == null) {
|
|
return;
|
|
}
|
|
List<Contact> contacts = binder.getContactsCopy();
|
|
if (i < 0 || i >= contacts.size()) {
|
|
return;
|
|
}
|
|
Contact contact = contacts.get(i);
|
|
if (!Utils.hasMicPermission(this)) {
|
|
Utils.requestMicPermission(this, MIC_PERMISSION_REQUEST_CODE);
|
|
return;
|
|
}
|
|
Intent intent = new Intent(this, CallActivity.class);
|
|
intent.setAction("ACTION_OUTGOING_CALL");
|
|
intent.putExtra("EXTRA_CONTACT", contact);
|
|
startActivity(intent);
|
|
}
|
|
|
|
@Override
|
|
protected void onCreate(Bundle savedInstanceState) {
|
|
log("onCreate");
|
|
super.onCreate(savedInstanceState);
|
|
setContentView(R.layout.activity_main);
|
|
|
|
fab = findViewById(R.id.fab);
|
|
fabScan = findViewById(R.id.fabScan);
|
|
fabGen = findViewById(R.id.fabGenerate);
|
|
contactListView = findViewById(R.id.contactList);
|
|
ownStatusText = findViewById(R.id.ownStatusText);
|
|
swipeRefreshLayout = findViewById(R.id.swipe_refresh);
|
|
|
|
swipeRefreshLayout.setOnRefreshListener(() -> refreshContactStatuses());
|
|
|
|
|
|
fabScan.setOnClickListener(v -> startActivity(new Intent(this, QRScanActivity.class)));
|
|
fabGen.setOnClickListener(v -> startActivity(new Intent(this, QRShowActivity.class)));
|
|
fab.setOnClickListener(this::runFabAnimation);
|
|
|
|
// ask for audio recording permissions
|
|
if (!Utils.hasMicPermission(this)) {
|
|
Utils.requestMicPermission(this, MIC_PERMISSION_REQUEST_CODE);
|
|
}
|
|
|
|
LocalBroadcastManager.getInstance(this).registerReceiver(refreshContactListReceiver, new IntentFilter("refresh_contact_list"));
|
|
LocalBroadcastManager.getInstance(this).registerReceiver(ownStatusReceiver, new IntentFilter(MainService.ACTION_OWN_STATUS_CHANGED));
|
|
}
|
|
|
|
private void refreshContactStatuses() {
|
|
if (!activityResumed || binder == null) {
|
|
stopSwipeRefresh();
|
|
return;
|
|
}
|
|
binder.checkOwnConnectivity();
|
|
binder.pingContacts();
|
|
eventBus.post(new RequestNodeStatusEvent());
|
|
statusRefreshHandler.removeCallbacks(refreshTimeoutRunnable);
|
|
statusRefreshHandler.postDelayed(refreshTimeoutRunnable, 15_000L);
|
|
}
|
|
|
|
private final Runnable refreshTimeoutRunnable = () -> stopSwipeRefresh();
|
|
|
|
private void stopSwipeRefresh() {
|
|
if (swipeRefreshLayout != null) {
|
|
swipeRefreshLayout.setRefreshing(false);
|
|
}
|
|
}
|
|
|
|
private void updateOwnStatusText() {
|
|
if (ownStatusText == null || !activityResumed) {
|
|
return;
|
|
}
|
|
String signaling;
|
|
if (binder == null) {
|
|
signaling = getString(R.string.own_status_checking);
|
|
} else {
|
|
switch (binder.getOwnStatus()) {
|
|
case LISTENING:
|
|
signaling = getString(R.string.own_status_ready);
|
|
break;
|
|
case IN_CALL:
|
|
signaling = getString(R.string.own_status_in_call);
|
|
break;
|
|
case NOT_LISTENING:
|
|
signaling = getString(R.string.own_status_not_listening);
|
|
break;
|
|
default:
|
|
signaling = getString(R.string.own_status_checking);
|
|
break;
|
|
}
|
|
}
|
|
String vpn = vpnOnline
|
|
? getString(R.string.own_status_vpn_online)
|
|
: getString(R.string.own_status_vpn_offline);
|
|
ownStatusText.setText(signaling + " · " + vpn);
|
|
}
|
|
|
|
void refreshContactList() {
|
|
log("refreshContactList");
|
|
if (binder == null) {
|
|
return;
|
|
}
|
|
|
|
new Handler(getMainLooper()).post(() -> {
|
|
if (binder == null || contactListView == null) {
|
|
return;
|
|
}
|
|
List<Contact> contacts = binder.getContactsCopy();
|
|
contactListView.setAdapter(new ContactListAdapter(this, R.layout.item_contact, contacts));
|
|
contactListView.setOnItemClickListener(this);
|
|
contactListView.setOnItemLongClickListener((AdapterView<?> adapterView, View view, int i, long l) -> {
|
|
if (i < 0 || i >= contacts.size()) {
|
|
return true;
|
|
}
|
|
Contact contact = contacts.get(i);
|
|
PopupMenu menu = new PopupMenu(this, view);
|
|
Resources res = getResources();
|
|
String delete = res.getString(R.string.delete);
|
|
String rename = res.getString(R.string.rename);
|
|
String block = res.getString(R.string.block);
|
|
String unblock = res.getString(R.string.unblock);
|
|
String share = res.getString(R.string.share);
|
|
String qr = "QR-ify";
|
|
|
|
menu.getMenu().add(delete);
|
|
menu.getMenu().add(rename);
|
|
menu.getMenu().add(share);
|
|
if (contact.getBlocked()) {
|
|
menu.getMenu().add(unblock);
|
|
} else {
|
|
menu.getMenu().add(block);
|
|
}
|
|
menu.getMenu().add(qr);
|
|
|
|
menu.setOnMenuItemClickListener((MenuItem menuItem) -> {
|
|
String title = menuItem.getTitle().toString();
|
|
byte[] publicKey = contact.getPublicKey();
|
|
if (title.equals(delete)) {
|
|
showDeleteDialog(publicKey, contact.getName());
|
|
} else if (title.equals(rename)) {
|
|
showContactEditDialog(publicKey, contact.getName());
|
|
} else if (title.equals(share)) {
|
|
shareContact(contact);
|
|
} else if (title.equals(block)) {
|
|
setBlocked(publicKey, true);
|
|
} else if (title.equals(unblock)) {
|
|
setBlocked(publicKey, false);
|
|
} else if (title.equals(qr)) {
|
|
Intent intent = new Intent(this, QRShowActivity.class);
|
|
intent.putExtra("EXTRA_CONTACT", contact);
|
|
startActivity(intent);
|
|
}
|
|
return false;
|
|
});
|
|
menu.show();
|
|
return true;
|
|
});
|
|
stopSwipeRefresh();
|
|
});
|
|
}
|
|
|
|
private void showDeleteDialog(byte[] publicKey, String name) {
|
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
|
|
|
builder.setTitle(R.string.confirm);
|
|
builder.setMessage(getResources().getString(R.string.contact_remove) + " " + name);
|
|
builder.setCancelable(false); // prevent key shortcut to cancel dialog
|
|
|
|
builder.setPositiveButton(R.string.yes, (DialogInterface dialog, int id) -> {
|
|
binder.deleteContact(publicKey);
|
|
dialog.cancel();
|
|
});
|
|
|
|
builder.setNegativeButton(R.string.no, (DialogInterface dialog, int id) -> {
|
|
dialog.cancel();
|
|
});
|
|
|
|
// create dialog box
|
|
AlertDialog alert = builder.create();
|
|
alert.show();
|
|
}
|
|
|
|
private void setBlocked(byte[] publicKey, boolean blocked) {
|
|
Contact contact = binder.getContactByPublicKey(publicKey);
|
|
if (contact != null) {
|
|
contact.setBlocked(blocked);
|
|
binder.addContact(contact);
|
|
}
|
|
}
|
|
|
|
private void shareContact(Contact c) {
|
|
log("shareContact");
|
|
try {
|
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
|
intent.setType("text/plain");
|
|
intent.putExtra(Intent.EXTRA_TEXT, Contact.exportJSON(c, false).toString());
|
|
startActivity(intent);
|
|
} catch (JSONException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
private void showContactEditDialog(byte[] publicKey, String name) {
|
|
log("showContactEditDialog");
|
|
EditText et = new EditText(this);
|
|
et.setText(name);
|
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
|
.setTitle(R.string.contact_edit)
|
|
.setView(et)
|
|
.setNegativeButton(getResources().getString(R.string.cancel), null)
|
|
.setPositiveButton(R.string.ok, (DialogInterface dialogInterface, int i) -> {
|
|
String newName = et.getText().toString();
|
|
if (newName.length() > 0) {
|
|
Contact contact = binder.getContactByPublicKey(publicKey);
|
|
if (contact != null) {
|
|
contact.setName(newName);
|
|
binder.addContact(contact);
|
|
}
|
|
}
|
|
}).show();
|
|
}
|
|
|
|
private void runFabAnimation(View fab) {
|
|
log("runFabAnimation");
|
|
AnimationSet scanSet = new AnimationSet(this, null);
|
|
AnimationSet generateSet = new AnimationSet(this, null);
|
|
|
|
final int distance = 200;
|
|
final int duration = 300;
|
|
TranslateAnimation scanAnimation;
|
|
TranslateAnimation generateAnimnation;
|
|
AlphaAnimation alphaAnimation;
|
|
|
|
if (fabExpanded) {
|
|
scanAnimation = new TranslateAnimation(0, 0, -distance, 0);
|
|
generateAnimnation = new TranslateAnimation(0, 0, -distance * 2, 0);
|
|
alphaAnimation = new AlphaAnimation(1.0f, 0.0f);
|
|
((FloatingActionButton) fab).setImageResource(R.drawable.qr_glass);
|
|
fabScan.setY(fabScan.getY() + distance);
|
|
fabGen.setY(fabGen.getY() + distance * 2);
|
|
} else {
|
|
scanAnimation = new TranslateAnimation(0, 0, distance, 0);
|
|
generateAnimnation = new TranslateAnimation(0, 0, distance * 2, 0);
|
|
alphaAnimation = new AlphaAnimation(0.0f, 1.0f);
|
|
((FloatingActionButton) fab).setImageResource(android.R.drawable.ic_menu_close_clear_cancel);
|
|
fabScan.setY(fabScan.getY() - distance);
|
|
fabGen.setY(fabGen.getY() - distance * 2);
|
|
}
|
|
|
|
scanSet.addAnimation(scanAnimation);
|
|
scanSet.addAnimation(alphaAnimation);
|
|
scanSet.setFillAfter(true);
|
|
|
|
generateSet.addAnimation(generateAnimnation);
|
|
generateSet.addAnimation(alphaAnimation);
|
|
generateSet.setFillAfter(true);
|
|
|
|
scanSet.setDuration(duration);
|
|
generateSet.setDuration(duration);
|
|
|
|
fabScan.show();
|
|
fabGen.show();
|
|
|
|
fabScan.startAnimation(scanSet);
|
|
fabGen.startAnimation(generateSet);
|
|
|
|
fabExpanded = !fabExpanded;
|
|
}
|
|
|
|
@Override
|
|
protected void onDestroy() {
|
|
log("onDestroy");
|
|
activityResumed = false;
|
|
statusRefreshHandler.removeCallbacks(statusRefreshRunnable);
|
|
statusRefreshHandler.removeCallbacks(refreshTimeoutRunnable);
|
|
if (serviceBound) {
|
|
try {
|
|
unbindService(this);
|
|
} catch (IllegalArgumentException ignored) {
|
|
}
|
|
serviceBound = false;
|
|
binder = null;
|
|
}
|
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(refreshContactListReceiver);
|
|
try {
|
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(ownStatusReceiver);
|
|
} catch (IllegalArgumentException ignored) {
|
|
}
|
|
((AudioManager)this.getSystemService(Context.AUDIO_SERVICE)).setSpeakerphoneOn(false);
|
|
super.onDestroy();
|
|
}
|
|
|
|
@Override
|
|
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
|
|
log("OnServiceConnected");
|
|
this.binder = (MainService.MainBinder) iBinder;
|
|
this.serviceBound = true;
|
|
refreshContactList();
|
|
updateOwnStatusText();
|
|
refreshContactStatuses();
|
|
}
|
|
|
|
@Override
|
|
public void onServiceDisconnected(ComponentName componentName) {
|
|
log("OnServiceDisconnected");
|
|
this.serviceBound = false;
|
|
this.binder = null;
|
|
updateOwnStatusText();
|
|
}
|
|
|
|
@Override
|
|
public boolean onOptionsItemSelected(MenuItem item) {
|
|
log("onOptionsItemSelected");
|
|
int id = item.getItemId();
|
|
|
|
switch (id) {
|
|
case R.id.action_settings: {
|
|
Intent intent = new Intent(this, SettingsActivity.class);
|
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
|
startActivity(intent);
|
|
break;
|
|
}
|
|
case R.id.action_backup: {
|
|
//startActivity(new Intent(this, BackupActivity.class));
|
|
break;
|
|
}
|
|
case R.id.action_about: {
|
|
startActivity(new Intent(this, AboutActivity.class));
|
|
break;
|
|
}
|
|
}
|
|
return super.onOptionsItemSelected(item);
|
|
}
|
|
|
|
private final BroadcastReceiver refreshContactListReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (contactListView == null || binder == null) {
|
|
return;
|
|
}
|
|
refreshContactList();
|
|
stopSwipeRefresh();
|
|
}
|
|
};
|
|
|
|
private final BroadcastReceiver ownStatusReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
if (activityResumed) {
|
|
updateOwnStatusText();
|
|
}
|
|
stopSwipeRefresh();
|
|
}
|
|
};
|
|
|
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
|
public void onNodeStatus(NodeStatusEvent event) {
|
|
NodeStatus status = event.getStatus();
|
|
if (status != null && status.isOnline()) {
|
|
statusRefreshHandler.removeCallbacks(vpnOfflineRunnable);
|
|
vpnOnline = true;
|
|
// Keep own ID/QR address aligned with current TUN IPv4.
|
|
if (binder != null && binder.getSettings() != null
|
|
&& Utils.syncIdentityFromTun(binder.getSettings())) {
|
|
try {
|
|
binder.saveDatabase();
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
} else if (status != null && !status.isOnline()) {
|
|
// NodeStatus.isOnline() is planet reachability, not VpnService/TUN state.
|
|
// During/after calls it can flap OFFLINE while the system VPN key stays up.
|
|
if (Utils.getFirstTunIpv4() != null) {
|
|
statusRefreshHandler.removeCallbacks(vpnOfflineRunnable);
|
|
vpnOnline = true;
|
|
} else if (binder != null && binder.getCurrentCall() != null) {
|
|
vpnOnline = true;
|
|
} else {
|
|
statusRefreshHandler.removeCallbacks(vpnOfflineRunnable);
|
|
statusRefreshHandler.postDelayed(vpnOfflineRunnable, VPN_OFFLINE_DEBOUNCE_MS);
|
|
}
|
|
}
|
|
updateOwnStatusText();
|
|
stopSwipeRefresh();
|
|
}
|
|
|
|
/** Prefer TUN presence over flaky NodeStatus for the VPN status line. */
|
|
private void refreshVpnFromTunnel() {
|
|
if (Utils.getFirstTunIpv4() != null) {
|
|
statusRefreshHandler.removeCallbacks(vpnOfflineRunnable);
|
|
if (!vpnOnline) {
|
|
vpnOnline = true;
|
|
updateOwnStatusText();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onStart() {
|
|
super.onStart();
|
|
if (!eventBus.isRegistered(this)) {
|
|
eventBus.register(this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onStop() {
|
|
super.onStop();
|
|
statusRefreshHandler.removeCallbacks(statusRefreshRunnable);
|
|
if (eventBus.isRegistered(this)) {
|
|
eventBus.unregister(this);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onResume() {
|
|
log("OnResume");
|
|
super.onResume();
|
|
activityResumed = true;
|
|
|
|
if (!serviceBound) {
|
|
bindService(new Intent(this, MainService.class), this, Service.BIND_AUTO_CREATE);
|
|
} else if (binder != null) {
|
|
refreshContactList();
|
|
refreshVpnFromTunnel();
|
|
ensureVpnServiceRunning();
|
|
updateOwnStatusText();
|
|
refreshContactStatuses();
|
|
}
|
|
eventBus.post(new RequestNodeStatusEvent());
|
|
statusRefreshHandler.removeCallbacks(statusRefreshRunnable);
|
|
statusRefreshHandler.postDelayed(statusRefreshRunnable, STATUS_REFRESH_INTERVAL_MS);
|
|
}
|
|
|
|
/**
|
|
* After a call crash/error the process may leave ZeroTier stopped while the UI
|
|
* still thinks VPN should be up. Restart the last-activated network if TUN is gone.
|
|
*/
|
|
private void ensureVpnServiceRunning() {
|
|
if (Utils.getFirstTunIpv4() != null) {
|
|
vpnOnline = true;
|
|
return;
|
|
}
|
|
try {
|
|
if (VpnService.prepare(this) != null) {
|
|
return;
|
|
}
|
|
NetworkDao networkDao = ((BackOneConnectApplication) getApplication())
|
|
.getDaoSession()
|
|
.getNetworkDao();
|
|
List<Network> activated = networkDao.queryBuilder()
|
|
.where(NetworkDao.Properties.LastActivated.eq(true))
|
|
.list();
|
|
if (activated == null || activated.isEmpty()) {
|
|
activated = networkDao.loadAll();
|
|
}
|
|
if (activated == null || activated.isEmpty()) {
|
|
return;
|
|
}
|
|
Network network = activated.get(0);
|
|
Intent intent = new Intent(this, ZeroTierOneService.class);
|
|
intent.putExtra(ZeroTierOneService.ZT1_NETWORK_ID, network.getNetworkId());
|
|
intent.putExtra(ZeroTierOneService.ZT1_USE_DEFAULT_ROUTE, network.getUseDefaultRoute());
|
|
startService(intent);
|
|
log("restarted VPN service for network " + network.getNetworkIdStr());
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onPause() {
|
|
log("onPause");
|
|
activityResumed = false;
|
|
super.onPause();
|
|
collapseFab();
|
|
statusRefreshHandler.removeCallbacks(statusRefreshRunnable);
|
|
statusRefreshHandler.removeCallbacks(refreshTimeoutRunnable);
|
|
}
|
|
|
|
private void collapseFab() {
|
|
if (fabExpanded) {
|
|
fab.setImageResource(R.drawable.qr_glass);
|
|
fabScan.clearAnimation();
|
|
fabGen.clearAnimation();
|
|
fabScan.setY(fabScan.getY() + 200);
|
|
fabGen.setY(fabGen.getY() + 200 * 2);
|
|
fabExpanded = false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
if (requestCode == MIC_PERMISSION_REQUEST_CODE
|
|
&& !Utils.allGranted(grantResults)) {
|
|
Toast.makeText(this, R.string.permission_mic, Toast.LENGTH_LONG).show();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean onCreateOptionsMenu(Menu menu) {
|
|
log("onCreateOptionsMenu");
|
|
getMenuInflater().inflate(R.menu.menu_main_activity, menu);
|
|
return true;
|
|
}
|
|
|
|
private void log(String s) {
|
|
Log.d(this, s);
|
|
}
|
|
}
|