almost works
This commit is contained in:
Executable
+416
@@ -0,0 +1,416 @@
|
||||
/* Copyright (c) 2009, Nathan Freitas, Orbot / The Guardian Project - http://openideals.com/guardian */
|
||||
/* See LICENSE for licensing information */
|
||||
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.*;
|
||||
import android.widget.AbsListView.OnScrollListener;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class AppManager extends Activity implements OnCheckedChangeListener,
|
||||
OnClickListener {
|
||||
|
||||
private static class ListEntry {
|
||||
private CheckBox box;
|
||||
private TextView text;
|
||||
private ImageView icon;
|
||||
}
|
||||
|
||||
public static ProxyedApp[] getProxyedApps(Context context) {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
|
||||
String tordAppString = prefs.getString(PREFS_KEY_PROXYED, "");
|
||||
String[] tordApps;
|
||||
|
||||
StringTokenizer st = new StringTokenizer(tordAppString, "|");
|
||||
tordApps = new String[st.countTokens()];
|
||||
int tordIdx = 0;
|
||||
while (st.hasMoreTokens()) {
|
||||
tordApps[tordIdx++] = st.nextToken();
|
||||
}
|
||||
|
||||
Arrays.sort(tordApps);
|
||||
|
||||
// else load the apps up
|
||||
PackageManager pMgr = context.getPackageManager();
|
||||
|
||||
List<ApplicationInfo> lAppInfo = pMgr.getInstalledApplications(0);
|
||||
|
||||
Iterator<ApplicationInfo> itAppInfo = lAppInfo.iterator();
|
||||
|
||||
Vector<ProxyedApp> vectorApps = new Vector<ProxyedApp>();
|
||||
|
||||
ApplicationInfo aInfo = null;
|
||||
|
||||
while (itAppInfo.hasNext()) {
|
||||
aInfo = itAppInfo.next();
|
||||
|
||||
// ignore system apps
|
||||
if (aInfo.uid < 10000)
|
||||
continue;
|
||||
|
||||
ProxyedApp app = new ProxyedApp();
|
||||
|
||||
app.setUid(aInfo.uid);
|
||||
|
||||
app.setUsername(pMgr.getNameForUid(app.getUid()));
|
||||
|
||||
// check if this application is allowed
|
||||
if (aInfo.packageName != null
|
||||
&& aInfo.packageName.equals("com.github.shadowsocks")) {
|
||||
app.setProxyed(true);
|
||||
} else if (Arrays.binarySearch(tordApps, app.getUsername()) >= 0) {
|
||||
app.setProxyed(true);
|
||||
} else {
|
||||
app.setProxyed(false);
|
||||
}
|
||||
|
||||
vectorApps.add(app);
|
||||
|
||||
}
|
||||
|
||||
ProxyedApp[] apps = new ProxyedApp[vectorApps.size()];
|
||||
vectorApps.toArray(apps);
|
||||
return apps;
|
||||
}
|
||||
|
||||
private ProxyedApp[] apps = null;
|
||||
|
||||
private ListView listApps;
|
||||
|
||||
private AppManager mAppManager;
|
||||
private TextView overlay;
|
||||
|
||||
private ProgressDialog pd = null;
|
||||
private ListAdapter adapter;
|
||||
|
||||
private ImageLoader dm;
|
||||
|
||||
private static final int MSG_LOAD_START = 1;
|
||||
|
||||
private static final int MSG_LOAD_FINISH = 2;
|
||||
|
||||
public final static String PREFS_KEY_PROXYED = "Proxyed";
|
||||
|
||||
private boolean appsLoaded = false;
|
||||
|
||||
final Handler handler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_LOAD_START:
|
||||
pd = ProgressDialog.show(AppManager.this, "",
|
||||
getString(R.string.loading), true, true);
|
||||
break;
|
||||
case MSG_LOAD_FINISH:
|
||||
|
||||
listApps.setAdapter(adapter);
|
||||
|
||||
listApps.setOnScrollListener(new OnScrollListener() {
|
||||
|
||||
boolean visible;
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView view,
|
||||
int firstVisibleItem, int visibleItemCount,
|
||||
int totalItemCount) {
|
||||
if (visible) {
|
||||
String name = apps[firstVisibleItem].getName();
|
||||
if (name != null && name.length() > 1)
|
||||
overlay.setText(apps[firstVisibleItem]
|
||||
.getName().substring(0, 1));
|
||||
else
|
||||
overlay.setText("*");
|
||||
overlay.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view,
|
||||
int scrollState) {
|
||||
visible = true;
|
||||
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
|
||||
overlay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (pd != null) {
|
||||
pd.dismiss();
|
||||
pd = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
public void getApps(Context context) {
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
|
||||
String tordAppString = prefs.getString(PREFS_KEY_PROXYED, "");
|
||||
String[] tordApps;
|
||||
|
||||
StringTokenizer st = new StringTokenizer(tordAppString, "|");
|
||||
tordApps = new String[st.countTokens()];
|
||||
int tordIdx = 0;
|
||||
while (st.hasMoreTokens()) {
|
||||
tordApps[tordIdx++] = st.nextToken();
|
||||
}
|
||||
|
||||
Arrays.sort(tordApps);
|
||||
|
||||
Vector<ProxyedApp> vectorApps = new Vector<ProxyedApp>();
|
||||
|
||||
// else load the apps up
|
||||
PackageManager pMgr = context.getPackageManager();
|
||||
|
||||
List<ApplicationInfo> lAppInfo = pMgr.getInstalledApplications(0);
|
||||
|
||||
Iterator<ApplicationInfo> itAppInfo = lAppInfo.iterator();
|
||||
|
||||
ApplicationInfo aInfo = null;
|
||||
|
||||
while (itAppInfo.hasNext()) {
|
||||
aInfo = itAppInfo.next();
|
||||
|
||||
// ignore system apps
|
||||
if (aInfo.uid < 10000)
|
||||
continue;
|
||||
|
||||
if (aInfo.processName == null)
|
||||
continue;
|
||||
if (pMgr.getApplicationLabel(aInfo) == null
|
||||
|| pMgr.getApplicationLabel(aInfo).toString().equals(""))
|
||||
continue;
|
||||
if (pMgr.getApplicationIcon(aInfo) == null)
|
||||
continue;
|
||||
|
||||
ProxyedApp tApp = new ProxyedApp();
|
||||
|
||||
tApp.setEnabled(aInfo.enabled);
|
||||
tApp.setUid(aInfo.uid);
|
||||
tApp.setUsername(pMgr.getNameForUid(tApp.getUid()));
|
||||
tApp.setProcname(aInfo.processName);
|
||||
tApp.setName(pMgr.getApplicationLabel(aInfo).toString());
|
||||
|
||||
// check if this application is allowed
|
||||
if (Arrays.binarySearch(tordApps, tApp.getUsername()) >= 0) {
|
||||
tApp.setProxyed(true);
|
||||
} else {
|
||||
tApp.setProxyed(false);
|
||||
}
|
||||
|
||||
vectorApps.add(tApp);
|
||||
}
|
||||
|
||||
apps = new ProxyedApp[vectorApps.size()];
|
||||
vectorApps.toArray(apps);
|
||||
|
||||
}
|
||||
|
||||
private void loadApps() {
|
||||
getApps(this);
|
||||
|
||||
Arrays.sort(apps, new Comparator<ProxyedApp>() {
|
||||
@Override
|
||||
public int compare(ProxyedApp o1, ProxyedApp o2) {
|
||||
if (o1 == null || o2 == null || o1.getName() == null
|
||||
|| o2.getName() == null)
|
||||
return 1;
|
||||
if (o1.isProxyed() == o2.isProxyed())
|
||||
return o1.getName().compareTo(o2.getName());
|
||||
if (o1.isProxyed())
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
|
||||
final LayoutInflater inflater = getLayoutInflater();
|
||||
|
||||
adapter = new ArrayAdapter<ProxyedApp>(this, R.layout.layout_apps_item,
|
||||
R.id.itemtext, apps) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ListEntry entry;
|
||||
if (convertView == null) {
|
||||
// Inflate a new view
|
||||
convertView = inflater.inflate(R.layout.layout_apps_item,
|
||||
parent, false);
|
||||
entry = new ListEntry();
|
||||
entry.icon = (ImageView) convertView
|
||||
.findViewById(R.id.itemicon);
|
||||
entry.box = (CheckBox) convertView
|
||||
.findViewById(R.id.itemcheck);
|
||||
entry.text = (TextView) convertView
|
||||
.findViewById(R.id.itemtext);
|
||||
|
||||
entry.text.setOnClickListener(mAppManager);
|
||||
entry.text.setOnClickListener(mAppManager);
|
||||
|
||||
convertView.setTag(entry);
|
||||
|
||||
entry.box.setOnCheckedChangeListener(mAppManager);
|
||||
} else {
|
||||
// Convert an existing view
|
||||
entry = (ListEntry) convertView.getTag();
|
||||
}
|
||||
|
||||
final ProxyedApp app = apps[position];
|
||||
|
||||
entry.icon.setTag(app.getUid());
|
||||
|
||||
dm.DisplayImage(app.getUid(),
|
||||
(Activity) convertView.getContext(), entry.icon);
|
||||
|
||||
entry.text.setText(app.getName());
|
||||
|
||||
final CheckBox box = entry.box;
|
||||
box.setTag(app);
|
||||
box.setChecked(app.isProxyed());
|
||||
|
||||
entry.text.setTag(box);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
};
|
||||
|
||||
appsLoaded = true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called an application is check/unchecked
|
||||
*/
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
final ProxyedApp app = (ProxyedApp) buttonView.getTag();
|
||||
if (app != null) {
|
||||
app.setProxyed(isChecked);
|
||||
}
|
||||
|
||||
saveAppSettings(this);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
CheckBox cbox = (CheckBox) v.getTag();
|
||||
|
||||
final ProxyedApp app = (ProxyedApp) cbox.getTag();
|
||||
if (app != null) {
|
||||
app.setProxyed(!app.isProxyed());
|
||||
cbox.setChecked(app.isProxyed());
|
||||
}
|
||||
|
||||
saveAppSettings(this);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
this.setContentView(R.layout.layout_apps);
|
||||
|
||||
this.dm = ImageLoaderFactory.getImageLoader(this);
|
||||
|
||||
this.overlay = (TextView) View.inflate(this, R.layout.overlay, null);
|
||||
getWindowManager()
|
||||
.addView(
|
||||
overlay,
|
||||
new WindowManager.LayoutParams(
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE,
|
||||
PixelFormat.TRANSLUCENT));
|
||||
|
||||
mAppManager = this;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
new Thread() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
handler.sendEmptyMessage(MSG_LOAD_START);
|
||||
|
||||
listApps = (ListView) findViewById(R.id.applistview);
|
||||
|
||||
if (!appsLoaded)
|
||||
loadApps();
|
||||
handler.sendEmptyMessage(MSG_LOAD_FINISH);
|
||||
}
|
||||
}.start();
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see android.app.Activity#onStop()
|
||||
*/
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
// Log.d(getClass().getName(),"Exiting Preferences");
|
||||
}
|
||||
|
||||
public void saveAppSettings(Context context) {
|
||||
if (apps == null)
|
||||
return;
|
||||
|
||||
SharedPreferences prefs = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
// final SharedPreferences prefs =
|
||||
// context.getSharedPreferences(PREFS_KEY, 0);
|
||||
|
||||
StringBuilder tordApps = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < apps.length; i++) {
|
||||
if (apps[i].isProxyed()) {
|
||||
tordApps.append(apps[i].getUsername());
|
||||
tordApps.append("|");
|
||||
}
|
||||
}
|
||||
|
||||
Editor edit = prefs.edit();
|
||||
edit.putString(PREFS_KEY_PROXYED, tordApps.toString());
|
||||
edit.commit();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* 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 com.github.shadowsocks;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
|
||||
/**
|
||||
* Utility methods for creating and managing a subprocess.
|
||||
* <p/>
|
||||
* Note: The native methods access a package-private java.io.FileDescriptor
|
||||
* field to get and set the raw Linux file descriptor. This might break if the
|
||||
* implementation of java.io.FileDescriptor is changed.
|
||||
*/
|
||||
|
||||
public class Exec {
|
||||
static {
|
||||
System.loadLibrary("exec");
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a given file descriptor.
|
||||
*/
|
||||
public static native void close(FileDescriptor fd);
|
||||
|
||||
/**
|
||||
* Create a subprocess. Differs from java.lang.ProcessBuilder in that a pty
|
||||
* is used to communicate with the subprocess.
|
||||
* <p/>
|
||||
* Callers are responsible for calling Exec.close() on the returned file
|
||||
* descriptor.
|
||||
*
|
||||
* @param cmd The command to execute
|
||||
* @param args An array of arguments to the command
|
||||
* @param envVars An array of strings of the form "VAR=value" to be added to the
|
||||
* environment of the process
|
||||
* @param processId A one-element array to which the process ID of the started
|
||||
* process will be written.
|
||||
* @return the file descriptor of the started process.
|
||||
*/
|
||||
public static native FileDescriptor createSubprocess(String cmd,
|
||||
String[] args, String[] envVars, int[] processId);
|
||||
|
||||
/**
|
||||
* Send SIGHUP to a process group.
|
||||
*/
|
||||
public static native void hangupProcessGroup(int processId);
|
||||
|
||||
/**
|
||||
* Causes the calling thread to wait for the process associated with the
|
||||
* receiver to finish executing.
|
||||
*
|
||||
* @return The exit value of the Process being waited on
|
||||
*/
|
||||
public static native int waitFor(int processId);
|
||||
}
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.R;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Stack;
|
||||
|
||||
public class ImageLoader {
|
||||
|
||||
// Used to display bitmap in the UI thread
|
||||
class BitmapDisplayer implements Runnable {
|
||||
Bitmap bitmap;
|
||||
ImageView imageView;
|
||||
|
||||
public BitmapDisplayer(Bitmap b, ImageView i) {
|
||||
bitmap = b;
|
||||
imageView = i;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (bitmap != null)
|
||||
imageView.setImageBitmap(bitmap);
|
||||
else
|
||||
imageView.setImageResource(stub_id);
|
||||
}
|
||||
}
|
||||
|
||||
class PhotosLoader extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
while (true) {
|
||||
// thread waits until there are any images to load in the
|
||||
// queue
|
||||
if (photosQueue.photosToLoad.size() == 0)
|
||||
synchronized (photosQueue.photosToLoad) {
|
||||
photosQueue.photosToLoad.wait();
|
||||
}
|
||||
if (photosQueue.photosToLoad.size() != 0) {
|
||||
PhotoToLoad photoToLoad;
|
||||
synchronized (photosQueue.photosToLoad) {
|
||||
photoToLoad = photosQueue.photosToLoad.pop();
|
||||
}
|
||||
Bitmap bmp = getBitmap(photoToLoad.uid);
|
||||
cache.put(photoToLoad.uid, bmp);
|
||||
Object tag = photoToLoad.imageView.getTag();
|
||||
if (tag != null && ((Integer) tag) == photoToLoad.uid) {
|
||||
BitmapDisplayer bd = new BitmapDisplayer(bmp,
|
||||
photoToLoad.imageView);
|
||||
Activity a = (Activity) photoToLoad.imageView
|
||||
.getContext();
|
||||
a.runOnUiThread(bd);
|
||||
}
|
||||
}
|
||||
if (Thread.interrupted())
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// allow thread to exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stores list of photos to download
|
||||
class PhotosQueue {
|
||||
private final Stack<PhotoToLoad> photosToLoad = new Stack<PhotoToLoad>();
|
||||
|
||||
// removes all instances of this ImageView
|
||||
public void Clean(ImageView image) {
|
||||
synchronized (photosToLoad) {
|
||||
for (int j = 0; j < photosToLoad.size(); ) {
|
||||
if (photosToLoad.get(j).imageView == image) photosToLoad.remove(j);
|
||||
else ++j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Task for the queue
|
||||
private class PhotoToLoad {
|
||||
public int uid;
|
||||
public ImageView imageView;
|
||||
|
||||
public PhotoToLoad(int u, ImageView i) {
|
||||
uid = u;
|
||||
imageView = i;
|
||||
}
|
||||
}
|
||||
|
||||
// the simplest in-memory cache implementation. This should be replaced with
|
||||
// something like SoftReference or BitmapOptions.inPurgeable(since 1.6)
|
||||
private HashMap<Integer, Bitmap> cache = new HashMap<Integer, Bitmap>();
|
||||
|
||||
private File cacheDir;
|
||||
|
||||
private Context context;
|
||||
|
||||
final int stub_id = R.drawable.sym_def_app_icon;
|
||||
|
||||
PhotosQueue photosQueue = new PhotosQueue();
|
||||
|
||||
PhotosLoader photoLoaderThread = new PhotosLoader();
|
||||
|
||||
public ImageLoader(Context c) {
|
||||
// Make the background thead low priority. This way it will not affect
|
||||
// the UI performance
|
||||
photoLoaderThread.setPriority(Thread.NORM_PRIORITY - 1);
|
||||
|
||||
context = c;
|
||||
|
||||
// Find the dir to save cached images
|
||||
cacheDir = context.getCacheDir();
|
||||
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
// clear memory cache
|
||||
cache.clear();
|
||||
|
||||
// clear SD cache
|
||||
File[] files = cacheDir.listFiles();
|
||||
for (File f : files)
|
||||
f.delete();
|
||||
}
|
||||
|
||||
// decodes image and scales it to reduce memory consumption
|
||||
private Bitmap decodeFile(File f) {
|
||||
try {
|
||||
// decode image size
|
||||
BitmapFactory.Options o = new BitmapFactory.Options();
|
||||
o.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(new FileInputStream(f), null, o);
|
||||
|
||||
// Find the correct scale value. It should be the power of 2.
|
||||
final int REQUIRED_SIZE = 70;
|
||||
int width_tmp = o.outWidth, height_tmp = o.outHeight;
|
||||
int scale = 1;
|
||||
while (true) {
|
||||
if (width_tmp / 2 < REQUIRED_SIZE
|
||||
|| height_tmp / 2 < REQUIRED_SIZE)
|
||||
break;
|
||||
width_tmp /= 2;
|
||||
height_tmp /= 2;
|
||||
scale *= 2;
|
||||
}
|
||||
|
||||
// decode with inSampleSize
|
||||
BitmapFactory.Options o2 = new BitmapFactory.Options();
|
||||
o2.inSampleSize = scale;
|
||||
return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
|
||||
} catch (FileNotFoundException e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void DisplayImage(int uid, Activity activity, ImageView imageView) {
|
||||
if (cache.containsKey(uid))
|
||||
imageView.setImageBitmap(cache.get(uid));
|
||||
else {
|
||||
queuePhoto(uid, activity, imageView);
|
||||
imageView.setImageResource(stub_id);
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getBitmap(int uid) {
|
||||
// I identify images by hashcode. Not a perfect solution, good for the
|
||||
// demo.
|
||||
String filename = String.valueOf(uid);
|
||||
File f = new File(cacheDir, filename);
|
||||
|
||||
// from SD cache
|
||||
Bitmap b = decodeFile(f);
|
||||
if (b != null)
|
||||
return b;
|
||||
|
||||
// from web
|
||||
try {
|
||||
BitmapDrawable icon = (BitmapDrawable) Utils.getAppIcon(context,
|
||||
uid);
|
||||
return icon.getBitmap();
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void queuePhoto(int uid, Activity activity, ImageView imageView) {
|
||||
// This ImageView may be used for other images before. So there may be
|
||||
// some old tasks in the queue. We need to discard them.
|
||||
photosQueue.Clean(imageView);
|
||||
PhotoToLoad p = new PhotoToLoad(uid, imageView);
|
||||
synchronized (photosQueue.photosToLoad) {
|
||||
photosQueue.photosToLoad.push(p);
|
||||
photosQueue.photosToLoad.notifyAll();
|
||||
}
|
||||
|
||||
// start thread if it's not started yet
|
||||
if (photoLoaderThread.getState() == Thread.State.NEW)
|
||||
photoLoaderThread.start();
|
||||
}
|
||||
|
||||
public void stopThread() {
|
||||
photoLoaderThread.interrupt();
|
||||
}
|
||||
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// dbartists - Douban artists client for Android
|
||||
// Copyright (C) 2011 Max Lv <max.c.lv@gmail.com>
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
//
|
||||
// ___====-_ _-====___
|
||||
// _--^^^#####// \\#####^^^--_
|
||||
// _-^##########// ( ) \\##########^-_
|
||||
// -############// |\^^/| \\############-
|
||||
// _/############// (@::@) \\############\_
|
||||
// /#############(( \\// ))#############\
|
||||
// -###############\\ (oo) //###############-
|
||||
// -#################\\ / VV \ //#################-
|
||||
// -###################\\/ \//###################-
|
||||
// _#/|##########/\######( /\ )######/\##########|\#_
|
||||
// |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
// ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
// ` ` ` ` / | | | | \ ' ' ' '
|
||||
// ( | | | | )
|
||||
// __\ | | | | /__
|
||||
// (vvv(VVV)(VVV)vvv)
|
||||
//
|
||||
// HERE BE DRAGONS
|
||||
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class ImageLoaderFactory {
|
||||
|
||||
private static ImageLoader il = null;
|
||||
|
||||
public static ImageLoader getImageLoader(Context context) {
|
||||
if (il == null) {
|
||||
il = new ImageLoader(context);
|
||||
}
|
||||
return il;
|
||||
}
|
||||
}
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
public class ProxyedApp {
|
||||
|
||||
private boolean enabled;
|
||||
private int uid;
|
||||
private String username;
|
||||
private String procname;
|
||||
private String name;
|
||||
|
||||
private boolean proxyed = false;
|
||||
|
||||
/**
|
||||
* @return the name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the procname
|
||||
*/
|
||||
public String getProcname() {
|
||||
return procname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the uid
|
||||
*/
|
||||
public int getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the username
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the enabled
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the proxyed
|
||||
*/
|
||||
public boolean isProxyed() {
|
||||
return proxyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param enabled the enabled to set
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name the name to set
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param procname the procname to set
|
||||
*/
|
||||
public void setProcname(String procname) {
|
||||
this.procname = procname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param proxyed the proxyed to set
|
||||
*/
|
||||
public void setProxyed(boolean proxyed) {
|
||||
this.proxyed = proxyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uid the uid to set
|
||||
*/
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param username the username to set
|
||||
*/
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
/* Shadowsocks - GoAgent / WallProxy client App for Android
|
||||
* Copyright (C) 2012 <max.c.lv@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.preference.*;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import com.google.analytics.tracking.android.EasyTracker;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class Shadowsocks extends PreferenceActivity implements
|
||||
OnSharedPreferenceChangeListener {
|
||||
|
||||
private static final String TAG = "Shadowsocks";
|
||||
|
||||
public static final String PREFS_NAME = "Shadowsocks";
|
||||
|
||||
private static final int MSG_CRASH_RECOVER = 1;
|
||||
private static final int MSG_INITIAL_FINISH = 2;
|
||||
|
||||
final Handler handler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(Shadowsocks.this);
|
||||
Editor ed = settings.edit();
|
||||
switch (msg.what) {
|
||||
case MSG_CRASH_RECOVER:
|
||||
Toast.makeText(Shadowsocks.this, R.string.crash_alert,
|
||||
Toast.LENGTH_LONG).show();
|
||||
ed.putBoolean("isRunning", false);
|
||||
break;
|
||||
case MSG_INITIAL_FINISH:
|
||||
if (mProgressDialog != null) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
ed.commit();
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
private static ProgressDialog mProgressDialog = null;
|
||||
|
||||
private CheckBoxPreference isAutoConnectCheck;
|
||||
private CheckBoxPreference isGlobalProxyCheck;
|
||||
private EditTextPreference proxyText;
|
||||
private EditTextPreference portText;
|
||||
private EditTextPreference remotePortText;
|
||||
private EditTextPreference sitekeyText;
|
||||
private CheckBoxPreference isGFWListCheck;
|
||||
private CheckBoxPreference isRunningCheck;
|
||||
private Preference proxyedApps;
|
||||
private CheckBoxPreference isBypassAppsCheck;
|
||||
|
||||
private void copyAssets(String path) {
|
||||
|
||||
AssetManager assetManager = getAssets();
|
||||
String[] files = null;
|
||||
try {
|
||||
files = assetManager.list(path);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
if (files != null) {
|
||||
for (String file : files) {
|
||||
InputStream in = null;
|
||||
OutputStream out = null;
|
||||
try {
|
||||
in = assetManager.open(file);
|
||||
out = new FileOutputStream("/data/data/com.github.shadowsocks/"
|
||||
+ file);
|
||||
copyFile(in, out);
|
||||
in.close();
|
||||
in = null;
|
||||
out.flush();
|
||||
out.close();
|
||||
out = null;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void copyFile(InputStream in, OutputStream out) throws IOException {
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
private void crash_recovery() {
|
||||
|
||||
Utils.runRootCommand(Utils.getIptables() + " -t nat -F OUTPUT");
|
||||
|
||||
Utils.runCommand(ShadowsocksService.BASE + "proxy.sh stop");
|
||||
|
||||
}
|
||||
|
||||
private void disableAll() {
|
||||
proxyText.setEnabled(false);
|
||||
portText.setEnabled(false);
|
||||
remotePortText.setEnabled(false);
|
||||
sitekeyText.setEnabled(false);
|
||||
proxyedApps.setEnabled(false);
|
||||
isGFWListCheck.setEnabled(false);
|
||||
isBypassAppsCheck.setEnabled(false);
|
||||
|
||||
isAutoConnectCheck.setEnabled(false);
|
||||
isGlobalProxyCheck.setEnabled(false);
|
||||
}
|
||||
|
||||
private void enableAll() {
|
||||
proxyText.setEnabled(true);
|
||||
portText.setEnabled(true);
|
||||
remotePortText.setEnabled(true);
|
||||
sitekeyText.setEnabled(true);
|
||||
isGlobalProxyCheck.setEnabled(true);
|
||||
isGFWListCheck.setEnabled(true);
|
||||
if (!isGlobalProxyCheck.isChecked()) {
|
||||
proxyedApps.setEnabled(true);
|
||||
isBypassAppsCheck.setEnabled(true);
|
||||
}
|
||||
|
||||
isAutoConnectCheck.setEnabled(true);
|
||||
}
|
||||
|
||||
private boolean isTextEmpty(String s, String msg) {
|
||||
if (s == null || s.length() <= 0) {
|
||||
showAToast(msg);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is first created.
|
||||
*/
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.main);
|
||||
addPreferencesFromResource(R.xml.shadowsocks_preference);
|
||||
|
||||
proxyText = (EditTextPreference) findPreference("proxy");
|
||||
portText = (EditTextPreference) findPreference("port");
|
||||
remotePortText = (EditTextPreference) findPreference("remotePort");
|
||||
sitekeyText = (EditTextPreference) findPreference("sitekey");
|
||||
proxyedApps = findPreference("proxyedApps");
|
||||
|
||||
isRunningCheck = (CheckBoxPreference) findPreference("isRunning");
|
||||
isAutoConnectCheck = (CheckBoxPreference) findPreference("isAutoConnect");
|
||||
isGlobalProxyCheck = (CheckBoxPreference) findPreference("isGlobalProxy");
|
||||
isGFWListCheck = (CheckBoxPreference) findPreference("isGFWList");
|
||||
isBypassAppsCheck = (CheckBoxPreference) findPreference("isBypassApps");
|
||||
|
||||
if (mProgressDialog == null)
|
||||
mProgressDialog = ProgressDialog.show(this, "",
|
||||
getString(R.string.initializing), true, true);
|
||||
|
||||
final SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
Utils.isRoot();
|
||||
|
||||
String versionName;
|
||||
try {
|
||||
versionName = getPackageManager().getPackageInfo(
|
||||
getPackageName(), 0).versionName;
|
||||
} catch (NameNotFoundException e) {
|
||||
versionName = "NONE";
|
||||
}
|
||||
|
||||
if (!settings.getBoolean(versionName, false)) {
|
||||
|
||||
Editor edit = settings.edit();
|
||||
edit.putBoolean(versionName, true);
|
||||
edit.commit();
|
||||
|
||||
reset();
|
||||
|
||||
}
|
||||
|
||||
handler.sendEmptyMessage(MSG_INITIAL_FINISH);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
menu.add(Menu.NONE, Menu.FIRST + 1, 1, getString(R.string.recovery))
|
||||
.setIcon(android.R.drawable.ic_menu_delete);
|
||||
menu.add(Menu.NONE, Menu.FIRST + 2, 2, getString(R.string.about))
|
||||
.setIcon(android.R.drawable.ic_menu_info_details);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
|
||||
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.putBoolean("isConnected", ShadowsocksService.isServiceStarted());
|
||||
editor.commit();
|
||||
|
||||
if (mProgressDialog != null) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { // 按下的如果是BACK,同时没有重复
|
||||
try {
|
||||
finish();
|
||||
} catch (Exception ignore) {
|
||||
// Nothing
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
// 菜单项被选择事件
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case Menu.FIRST + 1:
|
||||
recovery();
|
||||
break;
|
||||
case Menu.FIRST + 2:
|
||||
String versionName = "";
|
||||
try {
|
||||
versionName = getPackageManager().getPackageInfo(
|
||||
getPackageName(), 0).versionName;
|
||||
} catch (NameNotFoundException e) {
|
||||
versionName = "";
|
||||
}
|
||||
showAToast(getString(R.string.about) + " (" + versionName + ")\n\n"
|
||||
+ getString(R.string.copy_rights));
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Unregister the listener whenever a key changes
|
||||
getPreferenceScreen().getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
|
||||
Preference preference) {
|
||||
SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
if (preference.getKey() != null
|
||||
&& preference.getKey().equals("proxyedApps")) {
|
||||
Intent intent = new Intent(this, AppManager.class);
|
||||
startActivity(intent);
|
||||
} else if (preference.getKey() != null
|
||||
&& preference.getKey().equals("isRunning")) {
|
||||
if (!serviceStart()) {
|
||||
Editor edit = settings.edit();
|
||||
edit.putBoolean("isRunning", false);
|
||||
edit.commit();
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preferenceScreen, preference);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
if (settings.getBoolean("isSystemProxy", false)) {
|
||||
isGlobalProxyCheck.setEnabled(false);
|
||||
isGFWListCheck.setEnabled(false);
|
||||
proxyedApps.setEnabled(false);
|
||||
isBypassAppsCheck.setEnabled(false);
|
||||
} else {
|
||||
if (settings.getBoolean("isGlobalProxy", false)) {
|
||||
proxyedApps.setEnabled(false);
|
||||
isBypassAppsCheck.setEnabled(false);
|
||||
} else {
|
||||
proxyedApps.setEnabled(true);
|
||||
isBypassAppsCheck.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
sitekeyText.setEnabled(true);
|
||||
|
||||
Editor edit = settings.edit();
|
||||
|
||||
if (ShadowsocksService.isServiceStarted()) {
|
||||
edit.putBoolean("isRunning", true);
|
||||
} else {
|
||||
if (settings.getBoolean("isRunning", false)) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
crash_recovery();
|
||||
handler.sendEmptyMessage(MSG_CRASH_RECOVER);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
edit.putBoolean("isRunning", false);
|
||||
}
|
||||
|
||||
edit.commit();
|
||||
|
||||
if (settings.getBoolean("isRunning", false)) {
|
||||
isRunningCheck.setChecked(true);
|
||||
disableAll();
|
||||
} else {
|
||||
isRunningCheck.setChecked(false);
|
||||
enableAll();
|
||||
}
|
||||
|
||||
// Setup the initial values
|
||||
|
||||
if (!settings.getString("sitekey", "").equals(""))
|
||||
sitekeyText.setSummary(settings.getString("sitekey", ""));
|
||||
|
||||
if (!settings.getString("port", "").equals(""))
|
||||
portText.setSummary(settings.getString("port",
|
||||
getString(R.string.port_summary)));
|
||||
|
||||
if (!settings.getString("remotePort", "").equals(""))
|
||||
portText.setSummary(settings.getString("remotePort",
|
||||
getString(R.string.port_summary)));
|
||||
|
||||
if (!settings.getString("proxy", "").equals(""))
|
||||
proxyText.setSummary(settings.getString("proxy",
|
||||
getString(R.string.proxy_summary)));
|
||||
|
||||
// Set up a listener whenever a key changes
|
||||
getPreferenceScreen().getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
|
||||
String key) {
|
||||
// Let's do something a preference value changes
|
||||
SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
if (key.equals("isConnecting")) {
|
||||
if (settings.getBoolean("isConnecting", false)) {
|
||||
Log.d(TAG, "Connecting start");
|
||||
if (mProgressDialog == null)
|
||||
mProgressDialog = ProgressDialog.show(this, "",
|
||||
getString(R.string.connecting), true, true);
|
||||
} else {
|
||||
Log.d(TAG, "Connecting finish");
|
||||
if (mProgressDialog != null) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key.equals("isGlobalProxy")) {
|
||||
if (settings.getBoolean("isGlobalProxy", false)) {
|
||||
proxyedApps.setEnabled(false);
|
||||
isBypassAppsCheck.setEnabled(false);
|
||||
} else {
|
||||
proxyedApps.setEnabled(true);
|
||||
isBypassAppsCheck.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (key.equals("isRunning")) {
|
||||
if (settings.getBoolean("isRunning", false)) {
|
||||
disableAll();
|
||||
isRunningCheck.setChecked(true);
|
||||
} else {
|
||||
isRunningCheck.setChecked(false);
|
||||
enableAll();
|
||||
}
|
||||
}
|
||||
|
||||
if (key.equals("remotePort"))
|
||||
if (settings.getString("remotePort", "").equals(""))
|
||||
remotePortText.setSummary(getString(R.string.remote_port_summary));
|
||||
else
|
||||
remotePortText.setSummary(settings.getString("remotePort", ""));
|
||||
else if (key.equals("port"))
|
||||
if (settings.getString("port", "").equals(""))
|
||||
portText.setSummary(getString(R.string.port_summary));
|
||||
else
|
||||
portText.setSummary(settings.getString("port", ""));
|
||||
else if (key.equals("sitekey"))
|
||||
if (settings.getString("sitekey", "").equals(""))
|
||||
sitekeyText.setSummary(getString(R.string.sitekey_summary));
|
||||
else
|
||||
sitekeyText.setSummary(settings.getString("sitekey", ""));
|
||||
else if (key.equals("proxy"))
|
||||
if (settings.getString("proxy", "").equals("")) {
|
||||
proxyText.setSummary(getString(R.string.proxy_summary));
|
||||
} else {
|
||||
proxyText.setSummary(settings.getString("proxy", ""));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
EasyTracker.getInstance().activityStart(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
EasyTracker.getInstance().activityStop(this);
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Utils.getIptables()).append(" -t nat -F OUTPUT").append("\n");
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n");
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n");
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`").append("\n");
|
||||
sb.append("killall pdnsd").append("\n");
|
||||
sb.append("killall redsocks").append("\n");
|
||||
sb.append("killall shadowsocks").append("\n");
|
||||
Utils.runRootCommand(sb.toString());
|
||||
|
||||
copyAssets("");
|
||||
|
||||
Utils.runCommand("chmod 755 /data/data/com.github.shadowsocks/iptables\n"
|
||||
+ "chmod 755 /data/data/com.github.shadowsocks/redsocks\n"
|
||||
+ "chmod 755 /data/data/com.github.shadowsocks/pdnsd\n"
|
||||
+ "chmod 755 /data/data/com.github.shadowsocks/shadowsocks\n"
|
||||
);
|
||||
}
|
||||
|
||||
private void recovery() {
|
||||
|
||||
if (mProgressDialog == null)
|
||||
mProgressDialog = ProgressDialog.show(this, "", getString(R.string.recovering),
|
||||
true, true);
|
||||
|
||||
final Handler h = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (mProgressDialog != null) {
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
stopService(new Intent(this, ShadowsocksService.class));
|
||||
} catch (Exception e) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
reset();
|
||||
|
||||
h.sendEmptyMessage(0);
|
||||
}
|
||||
}.start();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when connect button is clicked.
|
||||
*/
|
||||
public boolean serviceStart() {
|
||||
|
||||
if (ShadowsocksService.isServiceStarted()) {
|
||||
try {
|
||||
stopService(new Intent(this, ShadowsocksService.class));
|
||||
} catch (Exception e) {
|
||||
// Nothing
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
SharedPreferences settings = PreferenceManager
|
||||
.getDefaultSharedPreferences(this);
|
||||
|
||||
final String proxy = settings.getString("proxy", "");
|
||||
if (isTextEmpty(proxy, getString(R.string.proxy_empty)))
|
||||
return false;
|
||||
|
||||
if (proxy.contains("proxyofmax.appspot.com")) {
|
||||
final TextView message = new TextView(this);
|
||||
message.setPadding(10, 5, 10, 5);
|
||||
final SpannableString s = new SpannableString(
|
||||
getText(R.string.default_proxy_alert));
|
||||
Linkify.addLinks(s, Linkify.WEB_URLS);
|
||||
message.setText(s);
|
||||
message.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.warning)
|
||||
.setCancelable(false)
|
||||
.setIcon(android.R.drawable.ic_dialog_info)
|
||||
.setNegativeButton(getString(R.string.ok_iknow),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
}).setView(message).create().show();
|
||||
}
|
||||
|
||||
String portText = settings.getString("port", "");
|
||||
if (isTextEmpty(portText, getString(R.string.port_empty)))
|
||||
return false;
|
||||
try {
|
||||
int port = Integer.valueOf(portText);
|
||||
if (port <= 1024) {
|
||||
this.showAToast(getString(R.string.port_alert));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
this.showAToast(getString(R.string.port_alert));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Intent it = new Intent(this, ShadowsocksService.class);
|
||||
startService(it);
|
||||
} catch (Exception e) {
|
||||
// Nothing
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showAToast(String msg) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(msg)
|
||||
.setCancelable(false)
|
||||
.setNegativeButton(getString(R.string.ok_iknow),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
dialog.cancel();
|
||||
}
|
||||
});
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.app.Application;
|
||||
import com.google.analytics.tracking.android.EasyTracker;
|
||||
|
||||
public class ShadowsocksApplication extends Application {
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
EasyTracker.getInstance().setContext(this);
|
||||
}
|
||||
|
||||
}
|
||||
+575
@@ -0,0 +1,575 @@
|
||||
/* gaeproxy - GAppProxy / WallProxy client App for Android
|
||||
* Copyright (C) 2011 <max.c.lv@gmail.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*
|
||||
* ___====-_ _-====___
|
||||
* _--^^^#####// \\#####^^^--_
|
||||
* _-^##########// ( ) \\##########^-_
|
||||
* -############// |\^^/| \\############-
|
||||
* _/############// (@::@) \\############\_
|
||||
* /#############(( \\// ))#############\
|
||||
* -###############\\ (oo) //###############-
|
||||
* -#################\\ / VV \ //#################-
|
||||
* -###################\\/ \//###################-
|
||||
* _#/|##########/\######( /\ )######/\##########|\#_
|
||||
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
|
||||
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
|
||||
* ` ` ` ` / | | | | \ ' ' ' '
|
||||
* ( | | | | )
|
||||
* __\ | | | | /__
|
||||
* (vvv(VVV)(VVV)vvv)
|
||||
*
|
||||
* HERE BE DRAGONS
|
||||
*
|
||||
*/
|
||||
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Message;
|
||||
import android.os.PowerManager;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import com.google.analytics.tracking.android.EasyTracker;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashSet;
|
||||
|
||||
public class ShadowsocksService extends Service {
|
||||
|
||||
private Notification notification;
|
||||
private NotificationManager notificationManager;
|
||||
private PendingIntent pendIntent;
|
||||
private PowerManager.WakeLock mWakeLock;
|
||||
|
||||
public static final String BASE = "/data/data/com.github.shadowsocks/";
|
||||
private static final int MSG_CONNECT_START = 0;
|
||||
private static final int MSG_CONNECT_FINISH = 1;
|
||||
private static final int MSG_CONNECT_SUCCESS = 2;
|
||||
private static final int MSG_CONNECT_FAIL = 3;
|
||||
private static final int MSG_HOST_CHANGE = 4;
|
||||
private static final int MSG_STOP_SELF = 5;
|
||||
|
||||
final static String CMD_IPTABLES_RETURN = " -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n";
|
||||
|
||||
final static String CMD_IPTABLES_REDIRECT_ADD_HTTP = " -t nat -A OUTPUT -p tcp "
|
||||
+ "--dport 80 -j REDIRECT --to 8123\n";
|
||||
final static String CMD_IPTABLES_REDIRECT_ADD_HTTPS = " -t nat -A OUTPUT -p tcp "
|
||||
+ "--dport 443 -j REDIRECT --to 8124\n";
|
||||
|
||||
final static String CMD_IPTABLES_DNAT_ADD_HTTP = " -t nat -A OUTPUT -p tcp "
|
||||
+ "--dport 80 -j DNAT --to-destination 127.0.0.1:8123\n";
|
||||
final static String CMD_IPTABLES_DNAT_ADD_HTTPS = " -t nat -A OUTPUT -p tcp "
|
||||
+ "--dport 443 -j DNAT --to-destination 127.0.0.1:8124\n";
|
||||
|
||||
private static final String TAG = "ShadowsocksService";
|
||||
private static final String DEFAULT_HOST = "74.125.128.18";
|
||||
private final static int DNS_PORT = 8053;
|
||||
|
||||
private Process httpProcess = null;
|
||||
private DataOutputStream httpOS = null;
|
||||
|
||||
private String appHost;
|
||||
private int remotePort;
|
||||
private int port;
|
||||
private String sitekey;
|
||||
|
||||
private SharedPreferences settings = null;
|
||||
|
||||
private boolean hasRedirectSupport = true;
|
||||
private boolean isGlobalProxy = false;
|
||||
private boolean isGFWList = false;
|
||||
private boolean isBypassApps = false;
|
||||
|
||||
private ProxyedApp apps[];
|
||||
|
||||
private static final Class<?>[] mStartForegroundSignature = new Class[]{
|
||||
int.class, Notification.class};
|
||||
private static final Class<?>[] mStopForegroundSignature = new Class[]{boolean.class};
|
||||
private static final Class<?>[] mSetForegroundSignature = new Class[]{boolean.class};
|
||||
|
||||
private Method mSetForeground;
|
||||
private Method mStartForeground;
|
||||
private Method mStopForeground;
|
||||
|
||||
private Object[] mSetForegroundArgs = new Object[1];
|
||||
private Object[] mStartForegroundArgs = new Object[2];
|
||||
private Object[] mStopForegroundArgs = new Object[1];
|
||||
|
||||
/*
|
||||
* This is a hack see
|
||||
* http://www.mail-archive.com/android-developers@googlegroups
|
||||
* .com/msg18298.html we are not really able to decide if the service was
|
||||
* started. So we remember a week reference to it. We set it if we are
|
||||
* running and clear it if we are stopped. If anything goes wrong, the
|
||||
* reference will hopefully vanish
|
||||
*/
|
||||
private static WeakReference<ShadowsocksService> sRunningInstance = null;
|
||||
|
||||
public static boolean isServiceStarted() {
|
||||
final boolean isServiceStarted;
|
||||
if (sRunningInstance == null) {
|
||||
isServiceStarted = false;
|
||||
} else if (sRunningInstance.get() == null) {
|
||||
isServiceStarted = false;
|
||||
sRunningInstance = null;
|
||||
} else {
|
||||
isServiceStarted = true;
|
||||
}
|
||||
return isServiceStarted;
|
||||
}
|
||||
|
||||
final Handler handler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
Editor ed = settings.edit();
|
||||
switch (msg.what) {
|
||||
case MSG_CONNECT_START:
|
||||
ed.putBoolean("isConnecting", true);
|
||||
|
||||
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||
mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK
|
||||
| PowerManager.ON_AFTER_RELEASE, "GAEProxy");
|
||||
|
||||
mWakeLock.acquire();
|
||||
|
||||
break;
|
||||
case MSG_CONNECT_FINISH:
|
||||
ed.putBoolean("isConnecting", false);
|
||||
|
||||
if (mWakeLock != null && mWakeLock.isHeld())
|
||||
mWakeLock.release();
|
||||
|
||||
break;
|
||||
case MSG_CONNECT_SUCCESS:
|
||||
ed.putBoolean("isRunning", true);
|
||||
break;
|
||||
case MSG_CONNECT_FAIL:
|
||||
ed.putBoolean("isRunning", false);
|
||||
break;
|
||||
case MSG_HOST_CHANGE:
|
||||
ed.putString("appHost", appHost);
|
||||
break;
|
||||
case MSG_STOP_SELF:
|
||||
stopSelf();
|
||||
break;
|
||||
}
|
||||
ed.commit();
|
||||
super.handleMessage(msg);
|
||||
}
|
||||
};
|
||||
|
||||
public void startShadowsocksDaemon() {
|
||||
final String cmd = String.format(BASE
|
||||
+ "shadowsocks \"%s\" \"%d\" \"%d\" \"%s\"",
|
||||
appHost, remotePort, port, sitekey);
|
||||
Utils.runRootCommand(cmd);
|
||||
}
|
||||
|
||||
public void startDnsDaemon() {
|
||||
final String cmd = BASE + "pdnsd -c " + BASE + "pdnsd.conf";
|
||||
Utils.runRootCommand(cmd);
|
||||
}
|
||||
|
||||
private String getVersionName() {
|
||||
String version;
|
||||
try {
|
||||
PackageInfo pi = getPackageManager().getPackageInfo(
|
||||
getPackageName(), 0);
|
||||
version = pi.versionName;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
version = "Package name not found";
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
public void handleCommand(Intent intent) {
|
||||
|
||||
if (intent == null) {
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
appHost = settings.getString("proxy", "127.0.0.1");
|
||||
sitekey = settings.getString("sitekey", "default");
|
||||
try {
|
||||
remotePort = Integer.valueOf(settings.getString("remotePort", "1984"));
|
||||
} catch (NumberFormatException ex) {
|
||||
remotePort = 1984;
|
||||
}
|
||||
try {
|
||||
port = Integer.valueOf(settings.getString("port", "1984"));
|
||||
} catch (NumberFormatException ex) {
|
||||
port = 1984;
|
||||
}
|
||||
|
||||
isGlobalProxy = settings.getBoolean("isGlobalProxy", false);
|
||||
isGFWList = settings.getBoolean("isGFWList", false);
|
||||
isBypassApps = settings.getBoolean("isBypassApps", false);
|
||||
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
handler.sendEmptyMessage(MSG_CONNECT_START);
|
||||
|
||||
Log.d(TAG, "IPTABLES: " + Utils.getIptables());
|
||||
|
||||
// Test for Redirect Support
|
||||
hasRedirectSupport = Utils.getHasRedirectSupport();
|
||||
|
||||
if (handleConnection()) {
|
||||
// Connection and forward successful
|
||||
notifyAlert(getString(R.string.forward_success),
|
||||
getString(R.string.service_running));
|
||||
|
||||
handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 500);
|
||||
|
||||
|
||||
} else {
|
||||
// Connection or forward unsuccessful
|
||||
notifyAlert(getString(R.string.forward_fail),
|
||||
getString(R.string.service_failed));
|
||||
|
||||
stopSelf();
|
||||
handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500);
|
||||
}
|
||||
|
||||
handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 500);
|
||||
|
||||
}
|
||||
}).start();
|
||||
markServiceStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is first created.
|
||||
*/
|
||||
public boolean handleConnection() {
|
||||
|
||||
startShadowsocksDaemon();
|
||||
startDnsDaemon();
|
||||
setupIptables();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void initSoundVibrateLights(Notification notification) {
|
||||
notification.sound = null;
|
||||
notification.defaults |= Notification.DEFAULT_LIGHTS;
|
||||
}
|
||||
|
||||
void invokeMethod(Method method, Object[] args) {
|
||||
try {
|
||||
method.invoke(this, mStartForegroundArgs);
|
||||
} catch (InvocationTargetException e) {
|
||||
// Should not happen.
|
||||
Log.w(TAG, "Unable to invoke method", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should not happen.
|
||||
Log.w(TAG, "Unable to invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void markServiceStarted() {
|
||||
sRunningInstance = new WeakReference<ShadowsocksService>(this);
|
||||
}
|
||||
|
||||
private void markServiceStopped() {
|
||||
sRunningInstance = null;
|
||||
}
|
||||
|
||||
private void notifyAlert(String title, String info) {
|
||||
notification.icon = R.drawable.ic_stat_gaeproxy;
|
||||
notification.tickerText = title;
|
||||
notification.flags = Notification.FLAG_ONGOING_EVENT;
|
||||
initSoundVibrateLights(notification);
|
||||
// notification.defaults = Notification.DEFAULT_SOUND;
|
||||
notification.setLatestEventInfo(this, getString(R.string.app_name),
|
||||
info, pendIntent);
|
||||
startForegroundCompat(1, notification);
|
||||
}
|
||||
|
||||
private void notifyAlert(String title, String info, int flags) {
|
||||
notification.icon = R.drawable.ic_stat_gaeproxy;
|
||||
notification.tickerText = title;
|
||||
notification.flags = flags;
|
||||
initSoundVibrateLights(notification);
|
||||
notification.setLatestEventInfo(this, getString(R.string.app_name),
|
||||
info, pendIntent);
|
||||
notificationManager.notify(0, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
EasyTracker.getTracker().trackEvent("service", "start",
|
||||
getVersionName(), 0L);
|
||||
|
||||
settings = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
notificationManager = (NotificationManager) this
|
||||
.getSystemService(NOTIFICATION_SERVICE);
|
||||
|
||||
Intent intent = new Intent(this, Shadowsocks.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
pendIntent = PendingIntent.getActivity(this, 0, intent, 0);
|
||||
notification = new Notification();
|
||||
|
||||
try {
|
||||
mStartForeground = getClass().getMethod("startForeground",
|
||||
mStartForegroundSignature);
|
||||
mStopForeground = getClass().getMethod("stopForeground",
|
||||
mStopForegroundSignature);
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Running on an older platform.
|
||||
mStartForeground = mStopForeground = null;
|
||||
}
|
||||
|
||||
try {
|
||||
mSetForeground = getClass().getMethod("setForeground",
|
||||
mSetForegroundSignature);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalStateException(
|
||||
"OS doesn't have Service.startForeground OR Service.setForeground!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the activity is closed.
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
|
||||
EasyTracker.getTracker().trackEvent("service", "stop",
|
||||
getVersionName(), 0L);
|
||||
|
||||
stopForegroundCompat(1);
|
||||
|
||||
notifyAlert(getString(R.string.forward_stop),
|
||||
getString(R.string.service_stopped),
|
||||
Notification.FLAG_AUTO_CANCEL);
|
||||
|
||||
try {
|
||||
if (httpOS != null) {
|
||||
httpOS.close();
|
||||
httpOS = null;
|
||||
}
|
||||
if (httpProcess != null) {
|
||||
httpProcess.destroy();
|
||||
httpProcess = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "HTTP Server close unexpected");
|
||||
}
|
||||
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
// Make sure the connection is closed, important here
|
||||
onDisconnect();
|
||||
|
||||
}
|
||||
}.start();
|
||||
|
||||
Editor ed = settings.edit();
|
||||
ed.putBoolean("isRunning", false);
|
||||
ed.putBoolean("isConnecting", false);
|
||||
ed.commit();
|
||||
|
||||
try {
|
||||
notificationManager.cancel(0);
|
||||
} catch (Exception ignore) {
|
||||
// Nothing
|
||||
}
|
||||
|
||||
super.onDestroy();
|
||||
|
||||
markServiceStopped();
|
||||
}
|
||||
|
||||
private void onDisconnect() {
|
||||
Utils.runRootCommand(Utils.getIptables() + " -t nat -F OUTPUT");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/pdnsd.pid`").append("\n");
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/redsocks.pid`").append("\n");
|
||||
sb.append("kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`").append("\n");
|
||||
Utils.runRootCommand(sb.toString());
|
||||
}
|
||||
|
||||
// This is the old onStart method that will be called on the pre-2.0
|
||||
// platform. On 2.0 or later we override onStartCommand() so this
|
||||
// method will not be called.
|
||||
@Override
|
||||
public void onStart(Intent intent, int startId) {
|
||||
|
||||
handleCommand(intent);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
handleCommand(intent);
|
||||
// We want this service to continue running until it is explicitly
|
||||
// stopped, so return sticky.
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private boolean setupIptables() {
|
||||
|
||||
StringBuilder init_sb = new StringBuilder();
|
||||
|
||||
StringBuilder http_sb = new StringBuilder();
|
||||
|
||||
StringBuilder https_sb = new StringBuilder();
|
||||
|
||||
init_sb.append(Utils.getIptables()).append(" -t nat -F OUTPUT\n");
|
||||
|
||||
if (hasRedirectSupport) {
|
||||
init_sb.append(Utils.getIptables()).append(" -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to ").append(DNS_PORT).append("\n");
|
||||
} else {
|
||||
init_sb.append(Utils.getIptables()).append(" -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:").append(DNS_PORT).append("\n");
|
||||
}
|
||||
|
||||
String cmd_bypass = Utils.getIptables() + CMD_IPTABLES_RETURN;
|
||||
|
||||
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "--dport " + remotePort));
|
||||
|
||||
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner "
|
||||
+ getApplicationInfo().uid));
|
||||
|
||||
if (isGFWList) {
|
||||
String[] chn_list = getResources().getStringArray(R.array.chn_list);
|
||||
|
||||
for (String item : chn_list) {
|
||||
init_sb.append(cmd_bypass.replace("0.0.0.0", item));
|
||||
}
|
||||
}
|
||||
if (isGlobalProxy || isBypassApps) {
|
||||
http_sb.append(hasRedirectSupport ? Utils.getIptables()
|
||||
+ CMD_IPTABLES_REDIRECT_ADD_HTTP : Utils.getIptables()
|
||||
+ CMD_IPTABLES_DNAT_ADD_HTTP);
|
||||
https_sb.append(hasRedirectSupport ? Utils.getIptables()
|
||||
+ CMD_IPTABLES_REDIRECT_ADD_HTTPS : Utils.getIptables()
|
||||
+ CMD_IPTABLES_DNAT_ADD_HTTPS);
|
||||
}
|
||||
if (!isGlobalProxy) {
|
||||
// for proxy specified apps
|
||||
if (apps == null || apps.length <= 0)
|
||||
apps = AppManager.getProxyedApps(this);
|
||||
|
||||
HashSet<Integer> uidSet = new HashSet<Integer>();
|
||||
for (ProxyedApp app : apps) {
|
||||
if (app.isProxyed()) {
|
||||
uidSet.add(app.getUid());
|
||||
}
|
||||
}
|
||||
for (int uid : uidSet) {
|
||||
if (!isBypassApps) {
|
||||
http_sb.append((hasRedirectSupport ? Utils.getIptables()
|
||||
+ CMD_IPTABLES_REDIRECT_ADD_HTTP : Utils.getIptables()
|
||||
+ CMD_IPTABLES_DNAT_ADD_HTTP).replace("-t nat",
|
||||
"-t nat -m owner --uid-owner " + uid));
|
||||
https_sb.append((hasRedirectSupport ? Utils.getIptables()
|
||||
+ CMD_IPTABLES_REDIRECT_ADD_HTTPS : Utils.getIptables()
|
||||
+ CMD_IPTABLES_DNAT_ADD_HTTPS).replace("-t nat",
|
||||
"-t nat -m owner --uid-owner " + uid));
|
||||
} else {
|
||||
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String init_rules = init_sb.toString();
|
||||
Utils.runRootCommand(init_rules, 30 * 1000);
|
||||
|
||||
String redt_rules = http_sb.toString();
|
||||
|
||||
redt_rules += https_sb.toString();
|
||||
|
||||
Utils.runRootCommand(redt_rules);
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a wrapper around the new startForeground method, using the older
|
||||
* APIs if it is not available.
|
||||
*/
|
||||
void startForegroundCompat(int id, Notification notification) {
|
||||
// If we have the new startForeground API, then use it.
|
||||
if (mStartForeground != null) {
|
||||
mStartForegroundArgs[0] = id;
|
||||
mStartForegroundArgs[1] = notification;
|
||||
invokeMethod(mStartForeground, mStartForegroundArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back on the old API.
|
||||
mSetForegroundArgs[0] = Boolean.TRUE;
|
||||
invokeMethod(mSetForeground, mSetForegroundArgs);
|
||||
notificationManager.notify(id, notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a wrapper around the new stopForeground method, using the older
|
||||
* APIs if it is not available.
|
||||
*/
|
||||
void stopForegroundCompat(int id) {
|
||||
// If we have the new stopForeground API, then use it.
|
||||
if (mStopForeground != null) {
|
||||
mStopForegroundArgs[0] = Boolean.TRUE;
|
||||
try {
|
||||
mStopForeground.invoke(this, mStopForegroundArgs);
|
||||
} catch (InvocationTargetException e) {
|
||||
// Should not happen.
|
||||
Log.w(TAG, "Unable to invoke stopForeground", e);
|
||||
} catch (IllegalAccessException e) {
|
||||
// Should not happen.
|
||||
Log.w(TAG, "Unable to invoke stopForeground", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back on the old API. Note to cancel BEFORE changing the
|
||||
// foreground state, since we could be killed at that point.
|
||||
notificationManager.cancel(id);
|
||||
mSetForegroundArgs[0] = Boolean.FALSE;
|
||||
invokeMethod(mSetForeground, mSetForegroundArgs);
|
||||
}
|
||||
|
||||
}
|
||||
Executable
+427
@@ -0,0 +1,427 @@
|
||||
package com.github.shadowsocks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class Utils {
|
||||
|
||||
/**
|
||||
* Internal thread used to execute scripts (as root or not).
|
||||
*/
|
||||
private static final class ScriptRunner extends Thread {
|
||||
private final File file;
|
||||
private final String script;
|
||||
private final StringBuilder res;
|
||||
private final boolean asroot;
|
||||
public int exitcode = -1;
|
||||
// private Process exec;
|
||||
private int mProcId;
|
||||
private FileDescriptor mTermFd;
|
||||
|
||||
/**
|
||||
* Creates a new script runner.
|
||||
*
|
||||
* @param file temporary script file
|
||||
* @param script script to run
|
||||
* @param res response output
|
||||
* @param asroot if true, executes the script as root
|
||||
*/
|
||||
public ScriptRunner(File file, String script, StringBuilder res,
|
||||
boolean asroot) {
|
||||
this.file = file;
|
||||
this.script = script;
|
||||
this.res = res;
|
||||
this.asroot = asroot;
|
||||
}
|
||||
|
||||
private int createSubprocess(int[] processId, String cmd) {
|
||||
ArrayList<String> argList = parse(cmd);
|
||||
String arg0 = argList.get(0);
|
||||
String[] args = argList.toArray(new String[1]);
|
||||
|
||||
mTermFd = Exec.createSubprocess(arg0, args, null, processId);
|
||||
return processId[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy this script runner
|
||||
*/
|
||||
@Override
|
||||
public synchronized void destroy() {
|
||||
try {
|
||||
Exec.hangupProcessGroup(mProcId);
|
||||
Exec.close(mTermFd);
|
||||
} catch (NoClassDefFoundError ignore) {
|
||||
// Nothing
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<String> parse(String cmd) {
|
||||
final int PLAIN = 0;
|
||||
final int WHITESPACE = 1;
|
||||
final int INQUOTE = 2;
|
||||
int state = WHITESPACE;
|
||||
ArrayList<String> result = new ArrayList<String>();
|
||||
int cmdLen = cmd.length();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < cmdLen; i++) {
|
||||
char c = cmd.charAt(i);
|
||||
if (state == PLAIN) {
|
||||
if (Character.isWhitespace(c)) {
|
||||
result.add(builder.toString());
|
||||
builder.delete(0, builder.length());
|
||||
state = WHITESPACE;
|
||||
} else if (c == '"') {
|
||||
state = INQUOTE;
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
} else if (state == WHITESPACE) {
|
||||
if (Character.isWhitespace(c)) {
|
||||
// do nothing
|
||||
} else if (c == '"') {
|
||||
state = INQUOTE;
|
||||
} else {
|
||||
state = PLAIN;
|
||||
builder.append(c);
|
||||
}
|
||||
} else if (state == INQUOTE) {
|
||||
if (c == '\\') {
|
||||
if (i + 1 < cmdLen) {
|
||||
i += 1;
|
||||
builder.append(cmd.charAt(i));
|
||||
}
|
||||
} else if (c == '"') {
|
||||
state = PLAIN;
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
result.add(builder.toString());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
new File(DEFOUT_FILE).createNewFile();
|
||||
file.createNewFile();
|
||||
final String abspath = file.getAbsolutePath();
|
||||
|
||||
// TODO: Rewrite this line
|
||||
// make sure we have execution permission on the script file
|
||||
// Runtime.getRuntime().exec("chmod 755 " + abspath).waitFor();
|
||||
|
||||
// Write the script to be executed
|
||||
final OutputStreamWriter out = new OutputStreamWriter(
|
||||
new FileOutputStream(file));
|
||||
out.write("#!/system/bin/sh\n");
|
||||
out.write(script);
|
||||
if (!script.endsWith("\n"))
|
||||
out.write("\n");
|
||||
out.write("exit\n");
|
||||
out.flush();
|
||||
out.close();
|
||||
|
||||
if (this.asroot) {
|
||||
// Create the "su" request to run the script
|
||||
// exec = Runtime.getRuntime().exec(
|
||||
// root_shell + " -c " + abspath);
|
||||
|
||||
int pid[] = new int[1];
|
||||
mProcId = createSubprocess(pid, root_shell + " -c "
|
||||
+ abspath);
|
||||
} else {
|
||||
// Create the "sh" request to run the script
|
||||
// exec = Runtime.getRuntime().exec(getShell() + " " +
|
||||
// abspath);
|
||||
|
||||
int pid[] = new int[1];
|
||||
mProcId = createSubprocess(pid, getShell() + " " + abspath);
|
||||
}
|
||||
|
||||
final InputStream stdout = new FileInputStream(DEFOUT_FILE);
|
||||
final byte buf[] = new byte[8192];
|
||||
int read = 0;
|
||||
|
||||
exitcode = Exec.waitFor(mProcId);
|
||||
|
||||
// Read stdout
|
||||
while (stdout.available() > 0) {
|
||||
read = stdout.read(buf);
|
||||
if (res != null)
|
||||
res.append(new String(buf, 0, read));
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
if (res != null)
|
||||
res.append("\n" + ex);
|
||||
} finally {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final static String TAG = "Shadowsocks";
|
||||
|
||||
public final static String DEFAULT_SHELL = "/system/bin/sh";
|
||||
public final static String DEFAULT_ROOT = "/system/bin/su";
|
||||
|
||||
public final static String ALTERNATIVE_ROOT = "/system/xbin/su";
|
||||
public final static String DEFAULT_IPTABLES = "/data/data/com.github.shadowsocks/iptables";
|
||||
public final static String ALTERNATIVE_IPTABLES = "/system/bin/iptables";
|
||||
public final static String SCRIPT_FILE = "/data/data/com.github.shadowsocks/script";
|
||||
public final static String DEFOUT_FILE = "/data/data/com.github.shadowsocks/defout";
|
||||
|
||||
public final static int TIME_OUT = -99;
|
||||
private static boolean initialized = false;
|
||||
private static int hasRedirectSupport = -1;
|
||||
|
||||
private static int isRoot = -1;
|
||||
private static String shell = null;
|
||||
private static String root_shell = null;
|
||||
private static String iptables = null;
|
||||
|
||||
private static String data_path = null;
|
||||
|
||||
private static void checkIptables() {
|
||||
|
||||
if (!isRoot()) {
|
||||
iptables = DEFAULT_IPTABLES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check iptables binary
|
||||
iptables = DEFAULT_IPTABLES;
|
||||
|
||||
String lines = null;
|
||||
|
||||
boolean compatible = false;
|
||||
boolean version = false;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String command = iptables + " --version\n" + iptables
|
||||
+ " -L -t nat -n\n" + "exit\n";
|
||||
|
||||
int exitcode = runScript(command, sb, 10 * 1000, true);
|
||||
|
||||
if (exitcode == TIME_OUT)
|
||||
return;
|
||||
|
||||
lines = sb.toString();
|
||||
|
||||
if (lines.contains("OUTPUT")) {
|
||||
compatible = true;
|
||||
}
|
||||
if (lines.contains("v1.4.")) {
|
||||
version = true;
|
||||
}
|
||||
|
||||
if (!compatible || !version) {
|
||||
iptables = ALTERNATIVE_IPTABLES;
|
||||
if (!new File(iptables).exists())
|
||||
iptables = "iptables";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static Drawable getAppIcon(Context c, int uid) {
|
||||
PackageManager pm = c.getPackageManager();
|
||||
Drawable appIcon = c.getResources().getDrawable(
|
||||
android.R.drawable.sym_def_app_icon);
|
||||
String[] packages = pm.getPackagesForUid(uid);
|
||||
|
||||
if (packages != null) {
|
||||
if (packages.length == 1) {
|
||||
try {
|
||||
ApplicationInfo appInfo = pm.getApplicationInfo(
|
||||
packages[0], 0);
|
||||
appIcon = pm.getApplicationIcon(appInfo);
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(c.getPackageName(),
|
||||
"No package found matching with the uid " + uid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(c.getPackageName(), "Package not found for uid " + uid);
|
||||
}
|
||||
|
||||
return appIcon;
|
||||
}
|
||||
|
||||
public static String getDataPath(Context ctx) {
|
||||
|
||||
if (data_path == null) {
|
||||
|
||||
if (Environment.MEDIA_MOUNTED.equals(Environment
|
||||
.getExternalStorageState())) {
|
||||
data_path = Environment.getExternalStorageDirectory()
|
||||
.getAbsolutePath();
|
||||
} else {
|
||||
data_path = ctx.getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
Log.d(TAG, "Python Data Path: " + data_path);
|
||||
}
|
||||
|
||||
return data_path;
|
||||
}
|
||||
|
||||
public static boolean getHasRedirectSupport() {
|
||||
if (hasRedirectSupport == -1)
|
||||
initHasRedirectSupported();
|
||||
return hasRedirectSupport == 1 ? true : false;
|
||||
}
|
||||
|
||||
public static String getIptables() {
|
||||
if (iptables == null)
|
||||
checkIptables();
|
||||
return iptables;
|
||||
}
|
||||
|
||||
private static String getShell() {
|
||||
if (shell == null) {
|
||||
shell = DEFAULT_SHELL;
|
||||
if (!new File(shell).exists())
|
||||
shell = "sh";
|
||||
}
|
||||
return shell;
|
||||
}
|
||||
|
||||
public static void initHasRedirectSupported() {
|
||||
|
||||
if (!Utils.isRoot())
|
||||
return;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String command = Utils.getIptables()
|
||||
+ " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154";
|
||||
|
||||
int exitcode = runScript(command, sb, 10 * 1000, true);
|
||||
|
||||
String lines = sb.toString();
|
||||
|
||||
hasRedirectSupport = 1;
|
||||
|
||||
// flush the check command
|
||||
Utils.runRootCommand(command.replace("-A", "-D"));
|
||||
|
||||
if (exitcode == TIME_OUT)
|
||||
return;
|
||||
|
||||
if (lines.contains("No chain/target/match")) {
|
||||
hasRedirectSupport = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInitialized() {
|
||||
if (initialized)
|
||||
return true;
|
||||
else {
|
||||
initialized = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static boolean isRoot() {
|
||||
|
||||
if (isRoot != -1)
|
||||
return isRoot == 1 ? true : false;
|
||||
|
||||
// switch between binaries
|
||||
if (new File(DEFAULT_ROOT).exists()) {
|
||||
root_shell = DEFAULT_ROOT;
|
||||
} else if (new File(ALTERNATIVE_ROOT).exists()) {
|
||||
root_shell = ALTERNATIVE_ROOT;
|
||||
} else {
|
||||
root_shell = "su";
|
||||
}
|
||||
|
||||
String lines = null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String command = "ls /\n" + "exit\n";
|
||||
|
||||
int exitcode = runScript(command, sb, 10 * 1000, true);
|
||||
|
||||
if (exitcode == TIME_OUT) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lines = sb.toString();
|
||||
|
||||
if (lines.contains("system")) {
|
||||
isRoot = 1;
|
||||
}
|
||||
|
||||
return isRoot == 1 ? true : false;
|
||||
}
|
||||
|
||||
public static boolean runCommand(String command) {
|
||||
|
||||
return runCommand(command, 10 * 1000);
|
||||
|
||||
}
|
||||
|
||||
public static boolean runCommand(String command, int timeout) {
|
||||
|
||||
Log.d(TAG, command);
|
||||
|
||||
runScript(command, null, timeout, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean runRootCommand(String command) {
|
||||
|
||||
return runRootCommand(command, 10 * 1000);
|
||||
|
||||
}
|
||||
|
||||
public static boolean runRootCommand(String command, int timeout) {
|
||||
|
||||
if (!isRoot())
|
||||
return false;
|
||||
|
||||
Log.d(TAG, command);
|
||||
|
||||
runScript(command, null, timeout, true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private synchronized static int runScript(String script, StringBuilder res,
|
||||
long timeout, boolean asroot) {
|
||||
final File file = new File(SCRIPT_FILE);
|
||||
final ScriptRunner runner = new ScriptRunner(file, script, res, asroot);
|
||||
runner.start();
|
||||
try {
|
||||
if (timeout > 0) {
|
||||
runner.join(timeout);
|
||||
} else {
|
||||
runner.join();
|
||||
}
|
||||
if (runner.isAlive()) {
|
||||
// Timed-out
|
||||
runner.destroy();
|
||||
runner.join(1000);
|
||||
return TIME_OUT;
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
return TIME_OUT;
|
||||
}
|
||||
return runner.exitcode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user