own orbothelper & fix receiver visibility (#961)

This commit is contained in:
m2049r 2024-10-17 13:01:12 +02:00 committed by GitHub
parent 41e84f2e29
commit c49351a8a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 719 additions and 11 deletions

View File

@ -41,7 +41,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import info.guardianproject.netcipher.proxy.MyOrbotHelper;
import info.guardianproject.netcipher.proxy.SignatureUtils;
import info.guardianproject.netcipher.proxy.StatusCallback;
import lombok.RequiredArgsConstructor;
@ -75,7 +75,7 @@ public class NetCipherHelper implements StatusCallback {
}
final private Context context;
final private OrbotHelper orbot;
final private MyOrbotHelper orbot;
@SuppressLint("StaticFieldLeak")
private static NetCipherHelper Instance;
@ -85,7 +85,7 @@ public class NetCipherHelper implements StatusCallback {
synchronized (NetCipherHelper.class) {
if (Instance == null) {
final Context applicationContext = context.getApplicationContext();
Instance = new NetCipherHelper(applicationContext, OrbotHelper.get(context).statusTimeout(5000));
Instance = new NetCipherHelper(applicationContext, MyOrbotHelper.get(context).statusTimeout(5000));
}
}
}
@ -99,9 +99,9 @@ public class NetCipherHelper implements StatusCallback {
private OkHttpClient client;
private void createTorClient(Intent statusIntent) {
String orbotStatus = statusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS);
String orbotStatus = statusIntent.getStringExtra(MyOrbotHelper.EXTRA_STATUS);
if (orbotStatus == null) throw new IllegalStateException("status is null");
if (!orbotStatus.equals(OrbotHelper.STATUS_ON))
if (!orbotStatus.equals(MyOrbotHelper.STATUS_ON))
throw new IllegalStateException("Orbot is not ON");
try {
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
@ -146,7 +146,7 @@ public class NetCipherHelper implements StatusCallback {
.addStatusCallback(me);
// deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED
ContextCompat.registerReceiver(me.context, orbotStatusReceiver, new IntentFilter(OrbotHelper.ACTION_STATUS), ContextCompat.RECEIVER_NOT_EXPORTED);
ContextCompat.registerReceiver(me.context, orbotStatusReceiver, new IntentFilter(MyOrbotHelper.ACTION_STATUS), ContextCompat.RECEIVER_EXPORTED);
me.startTor();
}
@ -272,7 +272,7 @@ public class NetCipherHelper implements StatusCallback {
hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE");
return null != SignatureUtils.validateBroadcastIntent(context,
OrbotHelper.getOrbotStartIntent(context),
MyOrbotHelper.getOrbotStartIntent(context),
hashes, false);
}
@ -382,9 +382,9 @@ public class NetCipherHelper implements StatusCallback {
private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(OrbotHelper.EXTRA_STATUS));
if (OrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
if (OrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(OrbotHelper.EXTRA_STATUS))) {
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS));
if (MyOrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
if (MyOrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS))) {
getInstance().onNotEnabled();
}
}
@ -392,6 +392,6 @@ public class NetCipherHelper implements StatusCallback {
};
public void installOrbot(Activity host) {
host.startActivity(OrbotHelper.getOrbotInstallIntent(context));
host.startActivity(MyOrbotHelper.getOrbotInstallIntent(context));
}
}

View File

@ -0,0 +1,708 @@
/*
* Copyright 2014-2016 Hans-Christoph Steiner
* Copyright 2012-2016 Nathan Freitas
* Portions Copyright (c) 2016 CommonsWare, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package info.guardianproject.netcipher.proxy;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Utility class to simplify setting up a proxy connection
* to Orbot.
* <p>
* If you are using classes in the info.guardianproject.netcipher.client
* package, call OrbotHelper.get(this).init(); from onCreate()
* of a custom Application subclass, or from some other guaranteed
* entry point to your app. At that point, the
* info.guardianproject.netcipher.client classes will be ready
* for use.
*/
public class MyOrbotHelper implements ProxyHelper {
private final static int REQUEST_CODE_STATUS = 100;
public final static String ORBOT_PACKAGE_NAME = "org.torproject.android";
public final static String ORBOT_MARKET_URI = "market://details?id=" + ORBOT_PACKAGE_NAME;
public final static String ORBOT_FDROID_URI = "https://f-droid.org/repository/browse/?fdid="
+ ORBOT_PACKAGE_NAME;
public final static String ORBOT_PLAY_URI = "https://play.google.com/store/apps/details?id="
+ ORBOT_PACKAGE_NAME;
public final static String DEFAULT_PROXY_HOST = "localhost";//"127.0.0.1";
public final static int DEFAULT_PROXY_HTTP_PORT = 8118;
public final static int DEFAULT_PROXY_SOCKS_PORT = 9050;
/**
* A request to Orbot to transparently start Tor services
*/
public final static String ACTION_START = "org.torproject.android.intent.action.START";
/**
* {@link Intent} send by Orbot with {@code ON/OFF/STARTING/STOPPING} status
* included as an {@link #EXTRA_STATUS} {@code String}. Your app should
* always receive {@code ACTION_STATUS Intent}s since any other app could
* start Orbot. Also, user-triggered starts and stops will also cause
* {@code ACTION_STATUS Intent}s to be broadcast.
*/
public final static String ACTION_STATUS = "org.torproject.android.intent.action.STATUS";
/**
* {@code String} that contains a status constant: {@link #STATUS_ON},
* {@link #STATUS_OFF}, {@link #STATUS_STARTING}, or
* {@link #STATUS_STOPPING}
*/
public final static String EXTRA_STATUS = "org.torproject.android.intent.extra.STATUS";
/**
* A {@link String} {@code packageName} for Orbot to direct its status reply
* to, used in {@link #ACTION_START} {@link Intent}s sent to Orbot
*/
public final static String EXTRA_PACKAGE_NAME = "org.torproject.android.intent.extra.PACKAGE_NAME";
public final static String EXTRA_PROXY_PORT_HTTP = "org.torproject.android.intent.extra.HTTP_PROXY_PORT";
public final static String EXTRA_PROXY_PORT_SOCKS = "org.torproject.android.intent.extra.SOCKS_PROXY_PORT";
/**
* All tor-related services and daemons are stopped
*/
public final static String STATUS_OFF = "OFF";
/**
* All tor-related services and daemons have completed starting
*/
public final static String STATUS_ON = "ON";
public final static String STATUS_STARTING = "STARTING";
public final static String STATUS_STOPPING = "STOPPING";
/**
* The user has disabled the ability for background starts triggered by
* apps. Fallback to the old Intent that brings up Orbot.
*/
public final static String STATUS_STARTS_DISABLED = "STARTS_DISABLED";
public final static String ACTION_START_TOR = "org.torproject.android.START_TOR";
public final static String ACTION_REQUEST_HS = "org.torproject.android.REQUEST_HS_PORT";
public final static int START_TOR_RESULT = 0x9234;
public final static int HS_REQUEST_CODE = 9999;
/*
private OrbotHelper() {
// only static utility methods, do not instantiate
}
*/
/**
* Test whether a {@link URL} is a Tor Hidden Service host name, also known
* as an ".onion address".
*
* @return whether the host name is a Tor .onion address
*/
public static boolean isOnionAddress(URL url) {
return url.getHost().endsWith(".onion");
}
/**
* Test whether a URL {@link String} is a Tor Hidden Service host name, also known
* as an ".onion address".
*
* @return whether the host name is a Tor .onion address
*/
public static boolean isOnionAddress(String urlString) {
try {
return isOnionAddress(new URL(urlString));
} catch (MalformedURLException e) {
return false;
}
}
/**
* Test whether a {@link Uri} is a Tor Hidden Service host name, also known
* as an ".onion address".
*
* @return whether the host name is a Tor .onion address
*/
public static boolean isOnionAddress(Uri uri) {
return uri.getHost().endsWith(".onion");
}
/**
* Check if the tor process is running. This method is very
* brittle, and is therefore deprecated in favor of using the
* {@link #ACTION_STATUS} {@code Intent} along with the
* {@link #requestStartTor(Context)} method.
*/
@Deprecated
public static boolean isOrbotRunning(Context context) {
int procId = TorServiceUtils.findProcessId(context);
return (procId != -1);
}
public static boolean isOrbotInstalled(Context context) {
return isAppInstalled(context, ORBOT_PACKAGE_NAME);
}
private static boolean isAppInstalled(Context context, String uri) {
try {
PackageManager pm = context.getPackageManager();
pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
public static void requestHiddenServiceOnPort(Activity activity, int port) {
Intent intent = new Intent(ACTION_REQUEST_HS);
intent.setPackage(ORBOT_PACKAGE_NAME);
intent.putExtra("hs_port", port);
activity.startActivityForResult(intent, HS_REQUEST_CODE);
}
/**
* First, checks whether Orbot is installed. If Orbot is installed, then a
* broadcast {@link Intent} is sent to request Orbot to start
* transparently in the background. When Orbot receives this {@code
* Intent}, it will immediately reply to the app that called this method
* with an {@link #ACTION_STATUS} {@code Intent} that is broadcast to the
* {@code packageName} of the provided {@link Context} (i.e. {@link
* Context#getPackageName()}.
* <p>
* That reply {@link #ACTION_STATUS} {@code Intent} could say that the user
* has disabled background starts with the status
* {@link #STATUS_STARTS_DISABLED}. That means that Orbot ignored this
* request. To directly prompt the user to start Tor, use
* {@link #requestShowOrbotStart(Activity)}, which will bring up
* Orbot itself for the user to manually start Tor. Orbot always broadcasts
* it's status, so your app will receive those no matter how Tor gets
* started.
*
* @param context the app {@link Context} will receive the reply
* @return whether the start request was sent to Orbot
* @see #requestShowOrbotStart(Activity activity)
*/
public static boolean requestStartTor(Context context) {
if (MyOrbotHelper.isOrbotInstalled(context)) {
Log.i("OrbotHelper", "requestStartTor " + context.getPackageName());
Intent intent = getOrbotStartIntent(context);
context.sendBroadcast(intent);
return true;
}
return false;
}
/**
* Gets an {@link Intent} for starting Orbot. Orbot will reply with the
* current status to the {@code packageName} of the app in the provided
* {@link Context} (i.e. {@link Context#getPackageName()}.
*/
public static Intent getOrbotStartIntent(Context context) {
Intent intent = new Intent(ACTION_START);
intent.setPackage(ORBOT_PACKAGE_NAME);
intent.putExtra(EXTRA_PACKAGE_NAME, context.getPackageName());
return intent;
}
/**
* Gets a barebones {@link Intent} for starting Orbot. This is deprecated
* in favor of {@link #getOrbotStartIntent(Context)}.
*/
@Deprecated
public static Intent getOrbotStartIntent() {
Intent intent = new Intent(ACTION_START);
intent.setPackage(ORBOT_PACKAGE_NAME);
return intent;
}
/**
* First, checks whether Orbot is installed, then checks whether Orbot is
* running. If Orbot is installed and not running, then an {@link Intent} is
* sent to request the user to start Orbot, which will show the main Orbot screen.
* The result will be returned in
* {@link Activity#onActivityResult(int requestCode, int resultCode, Intent data)}
* with a {@code requestCode} of {@code START_TOR_RESULT}
* <p>
* Orbot will also always broadcast the status of starting Tor via the
* {@link #ACTION_STATUS} Intent, no matter how it is started.
*
* @param activity the {@code Activity} that gets the result of the
* {@link #START_TOR_RESULT} request
* @return whether the start request was sent to Orbot
* @see #requestStartTor(Context context)
*/
public static boolean requestShowOrbotStart(Activity activity) {
if (MyOrbotHelper.isOrbotInstalled(activity)) {
if (!MyOrbotHelper.isOrbotRunning(activity)) {
Intent intent = getShowOrbotStartIntent();
activity.startActivityForResult(intent, START_TOR_RESULT);
return true;
}
}
return false;
}
public static Intent getShowOrbotStartIntent() {
Intent intent = new Intent(ACTION_START_TOR);
intent.setPackage(ORBOT_PACKAGE_NAME);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return intent;
}
public static Intent getOrbotInstallIntent(Context context) {
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(ORBOT_MARKET_URI));
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resInfos = pm.queryIntentActivities(intent, 0);
String foundPackageName = null;
for (ResolveInfo r : resInfos) {
Log.i("OrbotHelper", "market: " + r.activityInfo.packageName);
if (TextUtils.equals(r.activityInfo.packageName, FDROID_PACKAGE_NAME)
|| TextUtils.equals(r.activityInfo.packageName, PLAY_PACKAGE_NAME)) {
foundPackageName = r.activityInfo.packageName;
break;
}
}
if (foundPackageName == null) {
intent.setData(Uri.parse(ORBOT_FDROID_URI));
} else {
intent.setPackage(foundPackageName);
}
return intent;
}
@Override
public boolean isInstalled(Context context) {
return isOrbotInstalled(context);
}
@Override
public void requestStatus(Context context) {
isOrbotRunning(context);
}
@Override
public boolean requestStart(Context context) {
return requestStartTor(context);
}
@Override
public Intent getInstallIntent(Context context) {
return getOrbotInstallIntent(context);
}
@Override
public Intent getStartIntent(Context context) {
return getOrbotStartIntent();
}
@Override
public String getName() {
return "Orbot";
}
/* MLM additions */
private final Context context;
private final Handler handler;
private boolean isInstalled = false;
@Nullable
private Intent lastStatusIntent = null;
private Set<StatusCallback> statusCallbacks =
newSetFromMap(new WeakHashMap<StatusCallback, Boolean>());
private Set<InstallCallback> installCallbacks =
newSetFromMap(new WeakHashMap<InstallCallback, Boolean>());
private long statusTimeoutMs = 30000L;
private long installTimeoutMs = 60000L;
private boolean validateOrbot = true;
abstract public static class SimpleStatusCallback
implements StatusCallback {
@Override
public void onEnabled(Intent statusIntent) {
// no-op; extend and override if needed
}
@Override
public void onStarting() {
// no-op; extend and override if needed
}
@Override
public void onStopping() {
// no-op; extend and override if needed
}
@Override
public void onDisabled() {
// no-op; extend and override if needed
}
@Override
public void onNotYetInstalled() {
// no-op; extend and override if needed
}
}
/**
* Callback interface used for reporting the results of an
* attempt to install Orbot
*/
public interface InstallCallback {
void onInstalled();
void onInstallTimeout();
}
private static volatile MyOrbotHelper instance;
/**
* Retrieves the singleton, initializing if if needed
*
* @param context any Context will do, as we will hold onto
* the Application
* @return the singleton
*/
synchronized public static MyOrbotHelper get(Context context) {
if (instance == null) {
instance = new MyOrbotHelper(context);
}
return (instance);
}
/**
* Standard constructor
*
* @param context any Context will do; OrbotInitializer will hold
* onto the Application context
*/
private MyOrbotHelper(Context context) {
this.context = context.getApplicationContext();
this.handler = new Handler(Looper.getMainLooper());
}
/**
* Adds a StatusCallback to be called when we find out that
* Orbot is ready. If Orbot is ready for use, your callback
* will be called with onEnabled() immediately, before this
* method returns.
*
* @param cb a callback
* @return the singleton, for chaining
*/
public MyOrbotHelper addStatusCallback(StatusCallback cb) {
statusCallbacks.add(cb);
if (lastStatusIntent != null) {
String status =
lastStatusIntent.getStringExtra(MyOrbotHelper.EXTRA_STATUS);
if (status.equals(MyOrbotHelper.STATUS_ON)) {
cb.onEnabled(lastStatusIntent);
}
}
return (this);
}
/**
* Removes an existing registered StatusCallback.
*
* @param cb the callback to remove
* @return the singleton, for chaining
*/
public MyOrbotHelper removeStatusCallback(StatusCallback cb) {
statusCallbacks.remove(cb);
return (this);
}
/**
* Adds an InstallCallback to be called when we find out that
* Orbot is installed
*
* @param cb a callback
* @return the singleton, for chaining
*/
public MyOrbotHelper addInstallCallback(InstallCallback cb) {
installCallbacks.add(cb);
return (this);
}
/**
* Removes an existing registered InstallCallback.
*
* @param cb the callback to remove
* @return the singleton, for chaining
*/
public MyOrbotHelper removeInstallCallback(InstallCallback cb) {
installCallbacks.remove(cb);
return (this);
}
/**
* Sets how long of a delay, in milliseconds, after trying
* to get a status from Orbot before we give up.
* Defaults to 30000ms = 30 seconds = 0.000347222 days
*
* @param timeoutMs delay period in milliseconds
* @return the singleton, for chaining
*/
public MyOrbotHelper statusTimeout(long timeoutMs) {
statusTimeoutMs = timeoutMs;
return (this);
}
/**
* Sets how long of a delay, in milliseconds, after trying
* to install Orbot do we assume that it's not happening.
* Defaults to 60000ms = 60 seconds = 1 minute = 1.90259e-6 years
*
* @param timeoutMs delay period in milliseconds
* @return the singleton, for chaining
*/
public MyOrbotHelper installTimeout(long timeoutMs) {
installTimeoutMs = timeoutMs;
return (this);
}
/**
* By default, NetCipher ensures that the Orbot on the
* device is one of the official builds. Call this method
* to skip that validation. Mostly, this is for developers
* who have their own custom Orbot builds (e.g., for
* dedicated hardware).
*
* @return the singleton, for chaining
*/
public MyOrbotHelper skipOrbotValidation() {
validateOrbot = false;
return (this);
}
/**
* @return true if Orbot is installed (the last time we checked),
* false otherwise
*/
public boolean isInstalled() {
return (isInstalled);
}
/**
* Initializes the connection to Orbot, revalidating that it is installed
* and requesting fresh status broadcasts. This is best run in your app's
* {@link android.app.Application} subclass, in its
* {@link android.app.Application#onCreate()} method.
*
* @return true if initialization is proceeding, false if Orbot is not installed,
* or version of Orbot with a unofficial signing key is present.
*/
public boolean init() {
Intent orbot = MyOrbotHelper.getOrbotStartIntent(context);
if (validateOrbot) {
ArrayList<String> hashes = new ArrayList<String>();
// Tor Project signing key
hashes.add("A4:54:B8:7A:18:47:A8:9E:D7:F5:E7:0F:BA:6B:BA:96:F3:EF:29:C2:6E:09:81:20:4F:E3:47:BF:23:1D:FD:5B");
// f-droid.org signing key
hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE");
orbot =
SignatureUtils.validateBroadcastIntent(context, orbot,
hashes, false);
}
if (orbot != null) {
isInstalled = true;
handler.postDelayed(onStatusTimeout, statusTimeoutMs);
ContextCompat.registerReceiver(context, orbotStatusReceiver, new IntentFilter(MyOrbotHelper.ACTION_STATUS), ContextCompat.RECEIVER_EXPORTED);
context.sendBroadcast(orbot);
} else {
isInstalled = false;
for (StatusCallback cb : statusCallbacks) {
cb.onNotYetInstalled();
}
}
return (isInstalled);
}
/**
* Given that init() returned false, calling installOrbot()
* will trigger an attempt to install Orbot from an available
* distribution channel (e.g., the Play Store). Only call this
* if the user is expecting it, such as in response to tapping
* a dialog button or an action bar item.
* <p>
* Note that installation may take a long time, even if
* the user is proceeding with the installation, due to network
* speeds, waiting for user input, and so on. Either specify
* a long timeout, or consider the timeout to be merely advisory
* and use some other user input to cause you to try
* init() again after, presumably, Orbot has been installed
* and configured by the user.
* <p>
* If the user does install Orbot, we will attempt init()
* again automatically. Hence, you will probably need user input
* to tell you when the user has gotten Orbot up and going.
*
* @param host the Activity that is triggering this work
*/
public void installOrbot(Activity host) {
handler.postDelayed(onInstallTimeout, installTimeoutMs);
IntentFilter filter =
new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
context.registerReceiver(orbotInstallReceiver, filter);
host.startActivity(MyOrbotHelper.getOrbotInstallIntent(context));
}
private BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(),
MyOrbotHelper.ACTION_STATUS)) {
String status = intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS);
if (status.equals(MyOrbotHelper.STATUS_ON)) {
lastStatusIntent = intent;
handler.removeCallbacks(onStatusTimeout);
for (StatusCallback cb : statusCallbacks) {
cb.onEnabled(intent);
}
} else if (status.equals(MyOrbotHelper.STATUS_OFF)) {
for (StatusCallback cb : statusCallbacks) {
cb.onDisabled();
}
} else if (status.equals(MyOrbotHelper.STATUS_STARTING)) {
for (StatusCallback cb : statusCallbacks) {
cb.onStarting();
}
} else if (status.equals(MyOrbotHelper.STATUS_STOPPING)) {
for (StatusCallback cb : statusCallbacks) {
cb.onStopping();
}
}
}
}
};
private Runnable onStatusTimeout = new Runnable() {
@Override
public void run() {
context.unregisterReceiver(orbotStatusReceiver);
for (StatusCallback cb : statusCallbacks) {
cb.onStatusTimeout();
}
}
};
private BroadcastReceiver orbotInstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (TextUtils.equals(intent.getAction(),
Intent.ACTION_PACKAGE_ADDED)) {
String pkgName = intent.getData().getEncodedSchemeSpecificPart();
if (MyOrbotHelper.ORBOT_PACKAGE_NAME.equals(pkgName)) {
isInstalled = true;
handler.removeCallbacks(onInstallTimeout);
context.unregisterReceiver(orbotInstallReceiver);
for (InstallCallback cb : installCallbacks) {
cb.onInstalled();
}
init();
}
}
}
};
private Runnable onInstallTimeout = new Runnable() {
@Override
public void run() {
context.unregisterReceiver(orbotInstallReceiver);
for (InstallCallback cb : installCallbacks) {
cb.onInstallTimeout();
}
}
};
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
static <E> Set<E> newSetFromMap(Map<E, Boolean> map) {
if (map.isEmpty()) {
return new SetFromMap<E>(map);
}
throw new IllegalArgumentException("map not empty");
}
}