listInfos = DBDao.query();
+ if (listInfos.size() == 0) {
+ Log.d(TAG, "reOpenDownLoad: NOTASK");
+
+ } else {
+ DownloadInfo info = listInfos.get(0);
+ DBDao.updateDownLoadState(4, 10, info.getUrl(), info.getPath());
+
+ mTitle = info.getTitle();
+ mArtist = info.getArtist();
+ mAlbum = info.getAlbum();
+ mExt = FileHelper.getExt(FileHelper.getFileName(info.getPath()), "mp4");
+
+ Log.d(TAG, "reOpenDownLoad: " + mTitle);
+
+
+ DOWNLOADLINK = info.getUrl();
+ runingTread = THREADCOUNT;
+ mCompletedSize = 0;
+
+ // 创建文件
+ if (NUtil.isExternalStorageExists()) {
+ try {
+ downloadFile = new File(QBaseApp
+ .getInstance().getOrCreateRoot("tmp")+ "/" + info.getPath());
+ } catch (NotFoundException e) {
+ Log.d(TAG, "NotFoundException:" + e.getMessage());
+ e.printStackTrace();
+ }
+
+ this.downloadNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ updateIntent = new Intent(this, getMan());
+ updatePendingIntent = PendingIntent.getActivity(this,
+ NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ Notification downloadNotification = NAction.getNotification(getApplicationContext(), mTitle + "(" + mArtist + ")", getString(R.string.up_soft_download), updatePendingIntent,
+ R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT);
+
+
+ downloadNotificationManager.notify(NotifyIndex,
+ downloadNotification);
+
+ DBDao = new DownloadLog(getApplicationContext());
+
+ try {
+ URL url = new URL(DOWNLOADLINK);
+ HttpURLConnection conn = (HttpURLConnection) url
+ .openConnection();
+ conn.setConnectTimeout(5000);
+ conn.setRequestMethod("GET");
+ int code = conn.getResponseCode();
+
+ if (code == 200) {
+ fileLenght = conn.getContentLength();
+ ISERORR = false;
+ RandomAccessFile raf = new RandomAccessFile(
+ downloadFile, "rwd");
+ raf.setLength(fileLenght);
+ raf.close();
+
+ sonThreadSize = fileLenght / THREADCOUNT;
+ isREAD = true;
+
+ DBDao.updatefileleng(fileLenght, DOWNLOADLINK, downloadFile.getName());
+
+ //DownloadInfo downdloadinfo = DBDao.getInfoByPath(downloadFile.getName());
+
+ for (int threadId = 1; threadId <= THREADCOUNT; threadId++) {
+
+ long start = (threadId - 1) * sonThreadSize;
+ long end = threadId * sonThreadSize;
+ if (THREADCOUNT != 1) {
+ end = end - 1;
+ }
+ if (threadId == THREADCOUNT) {
+ end = fileLenght;
+ }
+
+
+ /*if(!service_json.equals("")){
+ JSONObject jsonData=new JSONObject(service_json);
+ NStorage.setLongSP(getContext(), "download"+threadId, jsonData.getLong("download"+threadId));
+ }else{*/
+ NStorage.setLongSP(getContext(), "download" + threadId, 0);
+ //}
+
+ new DownloadThread(start, end, threadId,
+ DOWNLOADLINK).start();
+
+
+ }
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ } else {
+ Toast.makeText(getApplicationContext(), R.string.not_sd,
+ Toast.LENGTH_SHORT).show();
+ stopSelf();
+ }
+
+ }
+ }
+
+ public void onDestroy() {
+ Log.d(TAG, "onDestroy");
+ super.onDestroy();
+ }
+
+ class DownloadThread extends Thread {
+ int threadId;
+ private long start, end;
+ private String path;
+
+ public DownloadThread(long start, long end, int threadId, String path) {
+ this.start = start;
+ this.end = end;
+ this.threadId = threadId;
+ this.path = path;
+ }
+
+ @Override
+ public void run() {
+ int catched = 0;
+ try {
+ NAction.setThreadStat(getApplicationContext(), threadId, 1);
+ Long done = NStorage.getLongSP(getApplicationContext(), "download" + threadId);
+ if (done > 0) {
+ synchronized (DownloaderBase.this) {
+ long oldAlldownData = done - (sonThreadSize * (threadId - 1));
+ mCompletedSize += oldAlldownData;
+ start = done;
+ }
+ }
+
+
+ URL url = new URL(path);
+ HttpURLConnection conn = (HttpURLConnection) url
+ .openConnection();
+ conn.setConnectTimeout(5000);
+ conn.setRequestMethod("GET");
+ conn.setRequestProperty("Range", "bytes=" + start + "-" + end);
+ int code = conn.getResponseCode();
+ if (code >= 200 || code < 400) {
+ InputStream is = conn.getInputStream();
+ RandomAccessFile raf = new RandomAccessFile(downloadFile,
+ "rwd");
+ raf.seek(start);
+
+ int len = 0;
+ long total = 0;
+ byte[] buffer = new byte[4096];
+ while ((len = is.read(buffer)) != -1) {
+ raf.write(buffer, 0, len);
+ total += len;
+ mCompletedSize += len;
+ synchronized (DownloaderBase.this) {
+
+ NStorage.setLongSP(getApplicationContext(), "download" + threadId, (total + start));
+ NStorage.setLongSP(getApplicationContext(), "downloadProgress", mCompletedSize * 100 / fileLenght);
+
+
+ if (mCompletedSize * 100 / fileLenght % 2 == 0) {
+
+ updatePendingIntent = PendingIntent.getActivity(DownloaderBase.this,
+ NOTIFICATION_ID, updateIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ Notification downloadNotification = NAction.getNotification(getApplicationContext(), mTitle + "(" + mArtist + ")", mCompletedSize * 100 / fileLenght + "%", updatePendingIntent,
+ R.drawable.ic_download_nb, null, Notification.FLAG_ONGOING_EVENT);
+
+ downloadNotificationManager.notify(NotifyIndex,
+ downloadNotification);
+ }
+ }
+
+
+ DownloadInfo dInfo = DBDao.getInfoByPath(downloadFile
+ .getName());
+
+ if (dInfo != null) {
+ if (dInfo.getStat() == 2) {
+ NStorage.setLongSP(getApplicationContext(), "download" + threadId, (total + start));
+ synchronized (DownloaderBase.this) {
+ servicePause();
+ }
+ break;
+ }
+ }
+ }
+ is.close();
+ raf.close();
+ } else {
+ updateHandler.obtainMessage(DOWNLOAD_EXCEPTION).sendToTarget();
+ }
+ catched = 1;
+ } catch (IOException e) {
+ updateHandler.obtainMessage(EXCEPTION_FILE_NOTFOUND).sendToTarget();
+
+ catched = 1;
+ } finally {
+
+ Log.d(TAG, "download run finally:" + catched);
+ if (catched != 1) {
+ updateHandler.obtainMessage(DOWNLOAD_EXCEPTION).sendToTarget();
+ } else {
+
+ synchronized (DownloaderBase.this) {
+ long done = NStorage.getLongSP(getApplicationContext(), "download" + threadId);
+ end = end - (sonThreadSize * (threadId - 1));
+ if ((done - (sonThreadSize * (threadId - 1)) >= end))
+ runingTread--;
+ if (runingTread == 0) {
+
+ for (int i = 1; i <= THREADCOUNT; i++) {
+ NStorage.setLongSP(getApplicationContext(), "download" + i, 0);
+ }
+ updateHandler.obtainMessage(DOWNLOAD_COMPLETE).sendToTarget();
+ }
+
+ if (NAction.isThreadsStop(getApplicationContext())) {
+ Log.d(TAG, "HERE");
+ if (showToast) {
+ updateHandler.obtainMessage(DOWNLOAD_PAUSE).sendToTarget();
+ showToast = false;
+ }
+
+ }
+ }
+ }
+ }
+ }
+ }
+
+ class sonThreadInfo {
+ private int start, end, state, done, threadId;
+
+ public int getStart() {
+ return start;
+ }
+
+ public void setStart(int start) {
+ this.start = start;
+ }
+
+ public int getEnd() {
+ return end;
+ }
+
+ public void setEnd(int end) {
+ this.end = end;
+ }
+
+ public int getState() {
+ return state;
+ }
+
+ public void setState(int state) {
+ this.state = state;
+ }
+
+ public int getDone() {
+ return done;
+ }
+
+ public void setDone(int done) {
+ this.done = done;
+ }
+
+ public int getThreadId() {
+ return threadId;
+ }
+
+ public void setThreadId(int threadId) {
+ this.threadId = threadId;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qbaselib/src/main/java/com/quseit/service/FloatWindowService.java b/qbaselib/src/main/java/com/quseit/service/FloatWindowService.java
new file mode 100644
index 00000000..6e5c4d97
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/service/FloatWindowService.java
@@ -0,0 +1,69 @@
+package com.quseit.service;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static com.quseit.config.BASE_CONF.TIME_SPAN;
+
+public class FloatWindowService extends Service {
+
+ private Handler handler = new Handler();
+ private Timer timer;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (timer == null) {
+ timer = new Timer();
+ timer.scheduleAtFixedRate(new RefreshTask(), 0L, (long) TIME_SPAN);
+ }
+ int result = super.onStartCommand(intent, flags, startId);
+ return result;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // Service被终止的同时也停止定时器继续运行
+ timer.cancel();
+ timer = null;
+ SpeedWindowManager.getInstance().removeAllWindow(getApplicationContext());
+ }
+
+ class RefreshTask extends TimerTask {
+
+ @Override
+ public void run() {
+ // 当前没有悬浮窗显示,则创建悬浮窗。
+ if (!SpeedWindowManager.getInstance().isWindowShowing()) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ SpeedWindowManager.getInstance().initData();
+ SpeedWindowManager.getInstance().createWindow(getApplicationContext());
+ }
+ });
+ }
+ // 当前有悬浮窗显示,则更新内存数据。
+ else {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ SpeedWindowManager.getInstance().updateViewData(getApplicationContext());
+ }
+ });
+ }
+ }
+
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/service/ResourceManager.java b/qbaselib/src/main/java/com/quseit/service/ResourceManager.java
new file mode 100644
index 00000000..383c29f7
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/service/ResourceManager.java
@@ -0,0 +1,47 @@
+/**
+ * This class takes care of managing resources for us. In our code, we
+ * can't use R, since the name of the package containing R will
+ * change. (This same code is used in both org.renpy.android and
+ * org.renpy.pygame.) So this is the next best thing.
+ */
+
+package com.quseit.service;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.view.View;
+
+public class ResourceManager {
+
+ private Activity act;
+ private Resources res;
+
+ public ResourceManager(Activity activity) {
+ act = activity;
+ res = act.getResources();
+ }
+
+ public int getIdentifier(String name, String kind) {
+ return res.getIdentifier(name, kind, act.getPackageName());
+ }
+
+ public String getString(String name) {
+
+ try {
+ return res.getString(getIdentifier(name, "string"));
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ public View inflateView(String name) {
+ int id = getIdentifier(name, "layout");
+ return act.getLayoutInflater().inflate(id, null);
+ }
+
+ public View getViewById(View v, String name) {
+ int id = getIdentifier(name, "id");
+ return v.findViewById(id);
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/service/SpeedWindowManager.java b/qbaselib/src/main/java/com/quseit/service/SpeedWindowManager.java
new file mode 100644
index 00000000..a930391a
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/service/SpeedWindowManager.java
@@ -0,0 +1,297 @@
+package com.quseit.service;
+
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.net.TrafficStats;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.widget.TextView;
+
+
+import com.quseit.android.R;
+import com.quseit.config.BASE_CONF;
+import com.quseit.util.PreferenceUtil;
+import com.quseit.view.SmallWindowView;
+import com.quseit.view.WindowView;
+
+import java.text.DecimalFormat;
+
+public class SpeedWindowManager {
+
+ private static SpeedWindowManager instance;
+ private WindowManager mWindowManager;
+ private WindowView mBigWindowView;
+ private WindowView mSmallWindowView;
+ private LayoutParams windowParams;
+ private TextView tvMobileTx;
+ private TextView tvMobileRx;
+ private TextView tvWlanTx;
+ private TextView tvWlanRx;
+ private TextView tvSum;
+ private long rxtxTotal = 0;
+ private long mobileRecvSum = 0;
+ private long mobileSendSum = 0;
+ private long wlanRecvSum = 0;
+ private long wlanSendSum = 0;
+ private long exitTime = 0;
+ private DecimalFormat showFloatFormat = new DecimalFormat("0.00");
+
+ public static SpeedWindowManager getInstance() {
+ if (instance == null) {
+ instance = new SpeedWindowManager();
+ }
+ return instance;
+ }
+
+ public void createWindow(final Context context) {
+ createWindow(context, BASE_CONF.SMALL_WINDOW_TYPE);
+ }
+
+ private void createWindow(final Context context, int type) {
+ final WindowManager windowManager = getWindowManager(context);
+ if (windowParams == null) {
+ windowParams = getWindowParams(context);
+ }
+
+ if (mSmallWindowView == null) {
+ mSmallWindowView = new SmallWindowView(context);
+ Drawable background = getCurrentBgDrawable(context);
+ setViewBg(background);
+ if (PreferenceUtil.getSingleton(context).getBoolean(BASE_CONF.SP_LOC)) {
+ setOnTouchListener(context, mSmallWindowView, BASE_CONF.BIG_WINDOW_TYPE);
+ } else {
+ setOnTouchListener(windowManager, context, mSmallWindowView, BASE_CONF.BIG_WINDOW_TYPE);
+ }
+ windowManager.addView(mSmallWindowView, windowParams);
+ }
+ tvSum = (TextView) mSmallWindowView.findViewById(R.id.tvSum);
+
+
+ }
+
+
+ private Drawable getCurrentBgDrawable(Context context) {
+ Drawable background;
+ int bgId;
+ if (PreferenceUtil.getSingleton(context).getBoolean(BASE_CONF.SP_BG, false)) {
+ bgId = R.drawable.trans_bg;
+ } else {
+ bgId = R.drawable.float_bg;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ background = context.getDrawable(bgId);
+ } else {
+ background = context.getResources().getDrawable(bgId);
+ }
+ return background;
+ }
+
+ public void initData() {
+ mobileRecvSum = TrafficStats.getMobileRxBytes();
+ mobileSendSum = TrafficStats.getMobileTxBytes();
+ wlanRecvSum = TrafficStats.getTotalRxBytes() - mobileRecvSum;
+ wlanSendSum = TrafficStats.getTotalTxBytes() - mobileSendSum;
+ rxtxTotal = TrafficStats.getTotalRxBytes()
+ + TrafficStats.getTotalTxBytes();
+ }
+
+ private LayoutParams getWindowParams(Context context) {
+ final WindowManager windowManager = getWindowManager(context);
+ Point sizePoint = new Point();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+ windowManager.getDefaultDisplay().getSize(sizePoint);
+ }
+ int screenWidth = sizePoint.x;
+ int screenHeight = sizePoint.y;
+ LayoutParams windowParams = new LayoutParams();
+ windowParams.type = LayoutParams.TYPE_SYSTEM_ERROR;
+ windowParams.format = PixelFormat.RGBA_8888;
+ windowParams.flags = LayoutParams.FLAG_LAYOUT_IN_SCREEN | LayoutParams.FLAG_NOT_FOCUSABLE | LayoutParams.FLAG_NOT_TOUCH_MODAL;
+ windowParams.gravity = Gravity.START | Gravity.TOP;
+ windowParams.width = LayoutParams.WRAP_CONTENT;
+ windowParams.height = LayoutParams.WRAP_CONTENT;
+ int x = PreferenceUtil.getSingleton(context).getInt(BASE_CONF.SP_X, -1);
+ int y = PreferenceUtil.getSingleton(context).getInt(BASE_CONF.SP_Y, -1);
+ if (x == -1 || y == -1) {
+ x = screenWidth/2;
+ y = 0;
+ }
+ windowParams.x = x;
+ windowParams.y = y;
+ return windowParams;
+ }
+
+ private void setOnTouchListener(final WindowManager windowManager, final Context context, final WindowView windowView, final int type) {
+ windowView.setOnTouchListener(new OnTouchListener() {
+ int lastX, lastY;
+ int paramX, paramY;
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ lastX = (int) event.getRawX();
+ lastY = (int) event.getRawY();
+ paramX = windowParams.x;
+ paramY = windowParams.y;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ int dx = (int) event.getRawX() - lastX;
+ int dy = (int) event.getRawY() - lastY;
+ windowParams.x = paramX + dx;
+ windowParams.y = paramY + dy;
+ // 更新悬浮窗位置
+ windowManager.updateViewLayout(windowView, windowParams);
+ return true;
+ case MotionEvent.ACTION_UP:
+ if ((System.currentTimeMillis() - exitTime) < BASE_CONF.CHANGE_DELAY) {
+ createWindow(context, type);
+ return true;
+ } else {
+ exitTime = System.currentTimeMillis();
+ }
+ break;
+ default:
+ break;
+ }
+ return false;
+ }
+ });
+ }
+
+ private void setOnTouchListener(final Context context, final WindowView windowView, final int type) {
+ windowView.setOnTouchListener(new OnTouchListener() {
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ if ((System.currentTimeMillis() - exitTime) < BASE_CONF.CHANGE_DELAY) {
+ createWindow(context, type);
+ return true;
+ } else {
+ exitTime = System.currentTimeMillis();
+ }
+ break;
+ default:
+ break;
+ }
+ return false;
+ }
+ });
+ }
+
+ public void setViewBg(Drawable background) {
+
+ if (mSmallWindowView != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mSmallWindowView.setBackground(background);
+ } else {
+ mSmallWindowView.setBackgroundDrawable(background);
+ }
+ }
+ }
+
+ private void removeWindow(Context context, WindowView windowView) {
+ if (windowView != null) {
+ WindowManager windowManager = getWindowManager(context);
+ windowManager.removeView(windowView);
+ }
+ }
+
+ public void removeAllWindow(Context context) {
+ removeWindow(context, mBigWindowView);
+ removeWindow(context, mSmallWindowView);
+ mBigWindowView = null;
+ mSmallWindowView = null;
+ }
+
+ public void updateViewData(Context context) {
+
+ long tempSum = TrafficStats.getTotalRxBytes()
+ + TrafficStats.getTotalTxBytes();
+ long rxtxLast = tempSum - rxtxTotal;
+ double totalSpeed = rxtxLast * 1000 / BASE_CONF.TIME_SPAN;
+ rxtxTotal = tempSum;
+ long tempMobileRx = TrafficStats.getMobileRxBytes();
+ long tempMobileTx = TrafficStats.getMobileTxBytes();
+ long tempWlanRx = TrafficStats.getTotalRxBytes() - tempMobileRx;
+ long tempWlanTx = TrafficStats.getTotalTxBytes() - tempMobileTx;
+ long mobileLastRecv = tempMobileRx - mobileRecvSum;
+ long mobileLastSend = tempMobileTx - mobileSendSum;
+ long wlanLastRecv = tempWlanRx - wlanRecvSum;
+ long wlanLastSend = tempWlanTx - wlanSendSum;
+ double mobileRecvSpeed = mobileLastRecv * 1000 / BASE_CONF.TIME_SPAN;
+ double mobileSendSpeed = mobileLastSend * 1000 / BASE_CONF.TIME_SPAN;
+ double wlanRecvSpeed = wlanLastRecv * 1000 / BASE_CONF.TIME_SPAN;
+ double wlanSendSpeed = wlanLastSend * 1000 / BASE_CONF.TIME_SPAN;
+ mobileRecvSum = tempMobileRx;
+ mobileSendSum = tempMobileTx;
+ wlanRecvSum = tempWlanRx;
+ wlanSendSum = tempWlanTx;
+ if (mBigWindowView != null) {
+ if (mobileRecvSpeed >= 0d) {
+ tvMobileRx.setText(showSpeed(mobileRecvSpeed));
+ }
+ if (mobileSendSpeed >= 0d) {
+ tvMobileTx.setText(showSpeed(mobileSendSpeed));
+ }
+ if (wlanRecvSpeed >= 0d) {
+ tvWlanRx.setText(showSpeed(wlanRecvSpeed));
+ }
+ if (wlanSendSpeed >= 0d) {
+ tvWlanTx.setText(showSpeed(wlanSendSpeed));
+ }
+ }
+ if (mSmallWindowView != null && totalSpeed >= 0d) {
+ tvSum.setText(showSpeed(totalSpeed));
+ }
+
+ }
+
+ private String showSpeed(double speed) {
+ String speedString;
+ if (speed >= 1048576d) {
+ speedString = showFloatFormat.format(speed / 1048576d) + "MB/s";
+ } else {
+ speedString = showFloatFormat.format(speed / 1024d) + "KB/s";
+ }
+ return speedString;
+ }
+
+ public boolean isWindowShowing() {
+ return mBigWindowView != null || mSmallWindowView != null;
+ }
+
+ private WindowManager getWindowManager(Context context) {
+ if (mWindowManager == null) {
+ mWindowManager = (WindowManager) context
+ .getSystemService(Context.WINDOW_SERVICE);
+ }
+ return mWindowManager;
+ }
+
+ public int getWindowX() {
+ return windowParams.x;
+ }
+
+ public int getWindowY() {
+ return windowParams.y;
+ }
+
+ public void fixWindow(Context context, boolean yes) {
+ if (yes) {
+ setOnTouchListener(context, mSmallWindowView == null ? mBigWindowView : mSmallWindowView, mSmallWindowView == null ? BASE_CONF.SMALL_WINDOW_TYPE : BASE_CONF.BIG_WINDOW_TYPE);
+ } else {
+ setOnTouchListener(getWindowManager(context), context, mSmallWindowView == null ? mBigWindowView : mSmallWindowView, mSmallWindowView == null ? BASE_CONF.SMALL_WINDOW_TYPE : BASE_CONF.BIG_WINDOW_TYPE);
+ }
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ACache.java b/qbaselib/src/main/java/com/quseit/util/ACache.java
new file mode 100644
index 00000000..8aebf348
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ACache.java
@@ -0,0 +1,874 @@
+/**
+ * Copyright (c) 2012-2013, Michael Yang 杨福海 (www.yangfuhai.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.
+ */
+package com.quseit.util;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.RandomAccessFile;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+public class ACache {
+ //public static final int TIME_HOUR = 60 * 60;
+ //public static final int TIME_DAY = TIME_HOUR * 24;
+ private static final int MAX_SIZE = 1000 * 1000 * 50; // 50 mb
+ private static final int MAX_COUNT = Integer.MAX_VALUE; // 不限制存放数据的数量
+ private static Map mInstanceMap = new HashMap();
+ private ACacheManager mCache;
+
+ public static ACache get(Context ctx) throws Exception {
+ return get(ctx, "ACache");
+ }
+
+ public static ACache get(Context ctx, String cacheName) throws Exception {
+ File f = new File(ctx.getCacheDir(), cacheName);
+ return get(f, MAX_SIZE, MAX_COUNT);
+ }
+
+ public static ACache get(File cacheDir) {
+ return get(cacheDir, MAX_SIZE, MAX_COUNT);
+ }
+
+ public static ACache get(Context ctx, long max_zise, int max_count) {
+ File f = new File(ctx.getCacheDir(), "ACache");
+ return get(f, max_zise, max_count);
+ }
+
+ public static ACache get(File cacheDir, long max_zise, int max_count) {
+ ACache manager = mInstanceMap.get(cacheDir.getAbsoluteFile() + myPid());
+ if (manager == null) {
+ manager = new ACache(cacheDir, max_zise, max_count);
+ mInstanceMap.put(cacheDir.getAbsolutePath() + myPid(), manager);
+ }
+ return manager;
+ }
+
+ private static String myPid() {
+ return "_" + android.os.Process.myPid();
+ }
+
+ private ACache(File cacheDir, long max_size, int max_count) {
+ if (!cacheDir.exists() && !cacheDir.mkdirs()) {
+ throw new RuntimeException("can't make dirs in "
+ + cacheDir.getAbsolutePath());
+ }
+ mCache = new ACacheManager(cacheDir, max_size, max_count);
+ }
+
+ // =======================================
+ // ============ String数据 读写 ==============
+ // =======================================
+
+ /**
+ * 保存 String数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的String数据
+ */
+ public void put(String key, String value) {
+ File file = mCache.newFile(key);
+ BufferedWriter out = null;
+ try {
+ out = new BufferedWriter(new FileWriter(file), 1024);
+ out.write(value);
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ if (out != null) {
+ try {
+ out.flush();
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ mCache.put(file);
+ }
+ }
+
+ /**
+ * 保存 String数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的String数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, String value, int saveTime) {
+ put(key, Utils.newStringWithDateInfo(saveTime, value));
+ }
+
+ /**
+ * 读取 String数据
+ *
+ * @param key
+ * @return String 数据
+ */
+ public String getAsString(String key) {
+ File file = mCache.get(key);
+ if (!file.exists())
+ return null;
+ boolean removeFile = false;
+ BufferedReader in = null;
+ try {
+ in = new BufferedReader(new FileReader(file));
+ String readString = "";
+ String currentLine;
+ while ((currentLine = in.readLine()) != null) {
+ readString += currentLine;
+ }
+ if (!Utils.isDue(readString)) {
+ return Utils.clearDateInfo(readString);
+ } else {
+ removeFile = true;
+ return null;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (removeFile)
+ remove(key);
+ }
+ }
+
+ // =======================================
+ // ============= JSONObject 数据 读写 ==============
+ // =======================================
+
+ /**
+ * 保存 JSONObject数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的JSON数据
+ */
+ public void put(String key, JSONObject value) {
+ put(key, value.toString());
+ }
+
+ /**
+ * 保存 JSONObject数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的JSONObject数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, JSONObject value, int saveTime) {
+ put(key, value.toString(), saveTime);
+ }
+
+ /**
+ * 读取JSONObject数据
+ *
+ * @param key
+ * @return JSONObject数据
+ */
+ public JSONObject getAsJSONObject(String key) {
+ String JSONString = getAsString(key);
+ try {
+ JSONObject obj = new JSONObject(JSONString);
+ return obj;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ // =======================================
+ // ============ JSONArray 数据 读写 =============
+ // =======================================
+
+ /**
+ * 保存 JSONArray数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的JSONArray数据
+ */
+ public void put(String key, JSONArray value) {
+ put(key, value.toString());
+ }
+
+ /**
+ * 保存 JSONArray数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的JSONArray数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, JSONArray value, int saveTime) {
+ put(key, value.toString(), saveTime);
+ }
+
+ /**
+ * 读取JSONArray数据
+ *
+ * @param key
+ * @return JSONArray数据
+ */
+ public JSONArray getAsJSONArray(String key) {
+ String JSONString = getAsString(key);
+ try {
+ JSONArray obj = new JSONArray(JSONString);
+ return obj;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ // =======================================
+ // ============== byte 数据 读写 =============
+ // =======================================
+
+ /**
+ * 保存 byte数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的数据
+ */
+ public void put(String key, byte[] value) {
+ File file = mCache.newFile(key);
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(file);
+ out.write(value);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (out != null) {
+ try {
+ out.flush();
+ out.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ mCache.put(file);
+ }
+ }
+
+ /**
+ * 保存 byte数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, byte[] value, int saveTime) {
+ put(key, Utils.newByteArrayWithDateInfo(saveTime, value));
+ }
+
+ /**
+ * 获取 byte 数据
+ *
+ * @param key
+ * @return byte 数据
+ */
+ public byte[] getAsBinary(String key) {
+ RandomAccessFile RAFile = null;
+ boolean removeFile = false;
+ try {
+ File file = mCache.get(key);
+ if (!file.exists())
+ return null;
+ RAFile = new RandomAccessFile(file, "r");
+ byte[] byteArray = new byte[(int) RAFile.length()];
+ RAFile.read(byteArray);
+ if (!Utils.isDue(byteArray)) {
+ return Utils.clearDateInfo(byteArray);
+ } else {
+ removeFile = true;
+ return null;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ if (RAFile != null) {
+ try {
+ RAFile.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ if (removeFile)
+ remove(key);
+ }
+ }
+
+ // =======================================
+ // ============= 序列化 数据 读写 ===============
+ // =======================================
+
+ /**
+ * 保存 Serializable数据 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的value
+ */
+ public void put(String key, Serializable value) {
+ put(key, value, -1);
+ }
+
+ /**
+ * 保存 Serializable数据到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的value
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, Serializable value, int saveTime) {
+ ByteArrayOutputStream baos = null;
+ ObjectOutputStream oos = null;
+ try {
+ baos = new ByteArrayOutputStream();
+ oos = new ObjectOutputStream(baos);
+ oos.writeObject(value);
+ byte[] data = baos.toByteArray();
+ if (saveTime != -1) {
+ put(key, data, saveTime);
+ } else {
+ put(key, data);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ try {
+ oos.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ /**
+ * 读取 Serializable数据
+ *
+ * @param key
+ * @return Serializable 数据
+ */
+ public Object getAsObject(String key) {
+ byte[] data = getAsBinary(key);
+ if (data != null) {
+ ByteArrayInputStream bais = null;
+ ObjectInputStream ois = null;
+ try {
+ bais = new ByteArrayInputStream(data);
+ ois = new ObjectInputStream(bais);
+ return ois.readObject();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ } finally {
+ try {
+ if (bais != null)
+ bais.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ try {
+ if (ois != null)
+ ois.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ return null;
+
+ }
+
+ // =======================================
+ // ============== bitmap 数据 读写 =============
+ // =======================================
+
+ /**
+ * 保存 bitmap 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的bitmap数据
+ */
+ public void put(String key, Bitmap value) {
+ put(key, Utils.Bitmap2Bytes(value));
+ }
+
+ /**
+ * 保存 bitmap 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的 bitmap 数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, Bitmap value, int saveTime) {
+ put(key, Utils.Bitmap2Bytes(value), saveTime);
+ }
+
+ /**
+ * 读取 bitmap 数据
+ *
+ * @param key
+ * @return bitmap 数据
+ */
+ public Bitmap getAsBitmap(String key) {
+ if (getAsBinary(key) == null) {
+ return null;
+ }
+ return Utils.Bytes2Bimap(getAsBinary(key));
+ }
+
+ // =======================================
+ // ============= drawable 数据 读写 =============
+ // =======================================
+
+ /**
+ * 保存 drawable 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的drawable数据
+ */
+ public void put(String key, Drawable value) {
+ put(key, Utils.drawable2Bitmap(value));
+ }
+
+ /**
+ * 保存 drawable 到 缓存中
+ *
+ * @param key
+ * 保存的key
+ * @param value
+ * 保存的 drawable 数据
+ * @param saveTime
+ * 保存的时间,单位:秒
+ */
+ public void put(String key, Drawable value, int saveTime) {
+ put(key, Utils.drawable2Bitmap(value), saveTime);
+ }
+
+ /**
+ * 读取 Drawable 数据
+ *
+ * @param key
+ * @return Drawable 数据
+ */
+ public Drawable getAsDrawable(String key) {
+ if (getAsBinary(key) == null) {
+ return null;
+ }
+ return Utils.bitmap2Drawable(Utils.Bytes2Bimap(getAsBinary(key)));
+ }
+
+ /**
+ * 获取缓存文件
+ *
+ * @param key
+ * @return value 缓存的文件
+ */
+ public File file(String key) {
+ File f = mCache.newFile(key);
+ if (f.exists())
+ return f;
+ return null;
+ }
+
+ /**
+ * 移除某个key
+ *
+ * @param key
+ * @return 是否移除成功
+ */
+ public boolean remove(String key) {
+ return mCache.remove(key);
+ }
+
+ /**
+ * 清除所有数据
+ */
+ public void clear() {
+ mCache.clear();
+ }
+
+ /**
+ * @title 缓存管理器
+ * @author 杨福海(michael) www.yangfuhai.com
+ * @version 1.0
+ */
+ public class ACacheManager {
+ private final AtomicLong cacheSize;
+ private final AtomicInteger cacheCount;
+ private final long sizeLimit;
+ private final int countLimit;
+ private final Map lastUsageDates = Collections
+ .synchronizedMap(new HashMap());
+ protected File cacheDir;
+
+ private ACacheManager(File cacheDir, long sizeLimit, int countLimit) {
+ this.cacheDir = cacheDir;
+ this.sizeLimit = sizeLimit;
+ this.countLimit = countLimit;
+ cacheSize = new AtomicLong();
+ cacheCount = new AtomicInteger();
+ calculateCacheSizeAndCacheCount();
+ }
+
+ /**
+ * 计算 cacheSize和cacheCount
+ */
+ private void calculateCacheSizeAndCacheCount() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ int size = 0;
+ int count = 0;
+ File[] cachedFiles = cacheDir.listFiles();
+ if (cachedFiles != null) {
+ for (File cachedFile : cachedFiles) {
+ size += calculateSize(cachedFile);
+ count += 1;
+ lastUsageDates.put(cachedFile,
+ cachedFile.lastModified());
+ }
+ cacheSize.set(size);
+ cacheCount.set(count);
+ }
+ }
+ }).start();
+ }
+
+ private void put(File file) {
+ int curCacheCount = cacheCount.get();
+ while (curCacheCount + 1 > countLimit) {
+ long freedSize = removeNext();
+ cacheSize.addAndGet(-freedSize);
+
+ curCacheCount = cacheCount.addAndGet(-1);
+ }
+ cacheCount.addAndGet(1);
+
+ long valueSize = calculateSize(file);
+ long curCacheSize = cacheSize.get();
+ while (curCacheSize + valueSize > sizeLimit) {
+ long freedSize = removeNext();
+ curCacheSize = cacheSize.addAndGet(-freedSize);
+ }
+ cacheSize.addAndGet(valueSize);
+
+ Long currentTime = System.currentTimeMillis();
+ file.setLastModified(currentTime);
+ lastUsageDates.put(file, currentTime);
+ }
+
+ private File get(String key) {
+ File file = newFile(key);
+ Long currentTime = System.currentTimeMillis();
+ file.setLastModified(currentTime);
+ lastUsageDates.put(file, currentTime);
+
+ return file;
+ }
+
+ private File newFile(String key) {
+ return new File(cacheDir, key.hashCode() + "");
+ }
+
+ private boolean remove(String key) {
+ File image = get(key);
+ return image.delete();
+ }
+
+ private void clear() {
+ lastUsageDates.clear();
+ cacheSize.set(0);
+ File[] files = cacheDir.listFiles();
+ if (files != null) {
+ for (File f : files) {
+ f.delete();
+ }
+ }
+ }
+
+ /**
+ * 移除旧的文件
+ *
+ * @return
+ */
+ private long removeNext() {
+ if (lastUsageDates.isEmpty()) {
+ return 0;
+ }
+
+ Long oldestUsage = null;
+ File mostLongUsedFile = null;
+ Set> entries = lastUsageDates.entrySet();
+ synchronized (lastUsageDates) {
+ for (Entry entry : entries) {
+ if (mostLongUsedFile == null) {
+ mostLongUsedFile = entry.getKey();
+ oldestUsage = entry.getValue();
+ } else {
+ Long lastValueUsage = entry.getValue();
+ if (lastValueUsage < oldestUsage) {
+ oldestUsage = lastValueUsage;
+ mostLongUsedFile = entry.getKey();
+ }
+ }
+ }
+ }
+
+ long fileSize = calculateSize(mostLongUsedFile);
+ if (mostLongUsedFile.delete()) {
+ lastUsageDates.remove(mostLongUsedFile);
+ }
+ return fileSize;
+ }
+
+ private long calculateSize(File file) {
+ return file.length();
+ }
+ }
+
+ /**
+ * @title 时间计算工具类
+ * @author 杨福海(michael) www.yangfuhai.com
+ * @version 1.0
+ */
+ private static class Utils {
+
+ /**
+ * 判断缓存的String数据是否到期
+ *
+ * @param str
+ * @return true:到期了 false:还没有到期
+ */
+ private static boolean isDue(String str) {
+ return isDue(str.getBytes());
+ }
+
+ /**
+ * 判断缓存的byte数据是否到期
+ *
+ * @param data
+ * @return true:到期了 false:还没有到期
+ */
+ private static boolean isDue(byte[] data) {
+ String[] strs = getDateInfoFromDate(data);
+ if (strs != null && strs.length == 2) {
+ String saveTimeStr = strs[0];
+ while (saveTimeStr.startsWith("0")) {
+ saveTimeStr = saveTimeStr
+ .substring(1, saveTimeStr.length());
+ }
+ long saveTime = Long.valueOf(saveTimeStr);
+ long deleteAfter = Long.valueOf(strs[1]);
+ if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String newStringWithDateInfo(int second, String strInfo) {
+ return createDateInfo(second) + strInfo;
+ }
+
+ private static byte[] newByteArrayWithDateInfo(int second, byte[] data2) {
+ byte[] data1 = createDateInfo(second).getBytes();
+ byte[] retdata = new byte[data1.length + data2.length];
+ System.arraycopy(data1, 0, retdata, 0, data1.length);
+ System.arraycopy(data2, 0, retdata, data1.length, data2.length);
+ return retdata;
+ }
+
+ private static String clearDateInfo(String strInfo) {
+ if (strInfo != null && hasDateInfo(strInfo.getBytes())) {
+ strInfo = strInfo.substring(strInfo.indexOf(mSeparator) + 1,
+ strInfo.length());
+ }
+ return strInfo;
+ }
+
+ private static byte[] clearDateInfo(byte[] data) {
+ if (hasDateInfo(data)) {
+ return copyOfRange(data, indexOf(data, mSeparator) + 1,
+ data.length);
+ }
+ return data;
+ }
+
+ private static boolean hasDateInfo(byte[] data) {
+ return data != null && data.length > 15 && data[13] == '-'
+ && indexOf(data, mSeparator) > 14;
+ }
+
+ private static String[] getDateInfoFromDate(byte[] data) {
+ if (hasDateInfo(data)) {
+ String saveDate = new String(copyOfRange(data, 0, 13));
+ String deleteAfter = new String(copyOfRange(data, 14,
+ indexOf(data, mSeparator)));
+ return new String[]{saveDate, deleteAfter};
+ }
+ return null;
+ }
+
+ private static int indexOf(byte[] data, char c) {
+ for (int i = 0; i < data.length; i++) {
+ if (data[i] == c) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static byte[] copyOfRange(byte[] original, int from, int to) {
+ int newLength = to - from;
+ if (newLength < 0)
+ throw new IllegalArgumentException(from + " > " + to);
+ byte[] copy = new byte[newLength];
+ System.arraycopy(original, from, copy, 0,
+ Math.min(original.length - from, newLength));
+ return copy;
+ }
+
+ private static final char mSeparator = ' ';
+
+ private static String createDateInfo(int second) {
+ String currentTime = System.currentTimeMillis() + "";
+ while (currentTime.length() < 13) {
+ currentTime = "0" + currentTime;
+ }
+ return currentTime + "-" + second + mSeparator;
+ }
+
+ /*
+ * Bitmap → byte[]
+ */
+ private static byte[] Bitmap2Bytes(Bitmap bm) {
+ if (bm == null) {
+ return null;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
+ return baos.toByteArray();
+ }
+
+ /*
+ * byte[] → Bitmap
+ */
+ private static Bitmap Bytes2Bimap(byte[] b) {
+ if (b.length == 0) {
+ return null;
+ }
+ return BitmapFactory.decodeByteArray(b, 0, b.length);
+ }
+
+ /*
+ * Drawable → Bitmap
+ */
+ private static Bitmap drawable2Bitmap(Drawable drawable) {
+ if (drawable == null) {
+ return null;
+ }
+ // 取 drawable 的长宽
+ int w = drawable.getIntrinsicWidth();
+ int h = drawable.getIntrinsicHeight();
+ // 取 drawable 的颜色格式
+ Bitmap.Config config = drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
+ : Bitmap.Config.RGB_565;
+ // 建立对应 bitmap
+ Bitmap bitmap = Bitmap.createBitmap(w, h, config);
+ // 建立对应 bitmap 的画布
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, w, h);
+ // 把 drawable 内容画到画布中
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ /*
+ * Bitmap → Drawable
+ */
+ @SuppressWarnings("deprecation")
+ private static Drawable bitmap2Drawable(Bitmap bm) {
+ if (bm == null) {
+ return null;
+ }
+ return new BitmapDrawable(bm);
+ }
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/Base64.java b/qbaselib/src/main/java/com/quseit/util/Base64.java
new file mode 100644
index 00000000..4968c01a
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/Base64.java
@@ -0,0 +1,101 @@
+package com.quseit.util;
+
+public class Base64 {
+
+ private static final String base64code = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "+/";
+
+ private static final int splitLinesAt = 76;
+
+ public static byte[] zeroPad(int length, byte[] bytes) {
+ byte[] padded = new byte[length]; // initialized to zero by JVM
+ System.arraycopy(bytes, 0, padded, 0, bytes.length);
+ return padded;
+ }
+
+ public static String encode(String string) {
+
+ String encoded = "";
+ byte[] stringArray;
+ try {
+ stringArray = string.getBytes("UTF-8"); // use appropriate encoding string!
+ } catch (Exception ignored) {
+ stringArray = string.getBytes(); // use locale default rather than croak
+ }
+ // determine how many padding bytes to add to the output
+ int paddingCount = (3 - (stringArray.length % 3)) % 3;
+ // add any necessary padding to the input
+ stringArray = zeroPad(stringArray.length + paddingCount, stringArray);
+ // process 3 bytes at a time, churning out 4 output bytes
+ // worry about CRLF insertions later
+ for (int i = 0; i < stringArray.length; i += 3) {
+ int j = ((stringArray[i] & 0xff) << 16) +
+ ((stringArray[i + 1] & 0xff) << 8) +
+ (stringArray[i + 2] & 0xff);
+ encoded = encoded + base64code.charAt((j >> 18) & 0x3f) +
+ base64code.charAt((j >> 12) & 0x3f) +
+ base64code.charAt((j >> 6) & 0x3f) +
+ base64code.charAt(j & 0x3f);
+ }
+ // replace encoded padding nulls with "="
+ return splitLines(encoded.substring(0, encoded.length() -
+ paddingCount) + "==".substring(0, paddingCount));
+
+ }
+
+ private final static char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
+
+ private static int[] toInt = new int[128];
+
+ static {
+ for(int i=0; i< ALPHABET.length; i++){
+ toInt[ALPHABET[i]]= i;
+ }
+ }
+ public static byte[] decode(String s){
+ int delta = s.endsWith( "==" ) ? 2 : s.endsWith( "=" ) ? 1 : 0;
+ byte[] buffer = new byte[s.length()*3/4 - delta];
+ int mask = 0xFF;
+ int index = 0;
+ for(int i=0; i< s.length(); i+=4){
+ int c0 = toInt[s.charAt( i )];
+ int c1 = toInt[s.charAt( i + 1)];
+ buffer[index++]= (byte)(((c0 << 2) | (c1 >> 4)) & mask);
+ if(index >= buffer.length){
+ return buffer;
+ }
+ int c2 = toInt[s.charAt( i + 2)];
+ buffer[index++]= (byte)(((c1 << 4) | (c2 >> 2)) & mask);
+ if(index >= buffer.length){
+ return buffer;
+ }
+ int c3 = toInt[s.charAt( i + 3 )];
+ buffer[index++]= (byte)(((c2 << 6) | c3) & mask);
+ }
+ return buffer;
+ }
+
+ public static String splitLines(String string) {
+
+ String lines = "";
+ for (int i = 0; i < string.length(); i += splitLinesAt) {
+
+ lines += string.substring(i, Math.min(string.length(), i + splitLinesAt));
+ lines += "\r\n";
+
+ }
+ return lines;
+
+ }
+ public static void main(String[] args) {
+
+ for (int i = 0; i < args.length; i++) {
+
+ System.err.println("encoding \"" + args[i] + "\"");
+ System.out.println(encode(args[i]));
+
+ }
+
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/Blur.java b/qbaselib/src/main/java/com/quseit/util/Blur.java
new file mode 100644
index 00000000..858fa46d
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/Blur.java
@@ -0,0 +1,264 @@
+package com.quseit.util;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Build.VERSION;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+
+// Slightly modified source from Nicolas Pomepuy
+// https://github.com/PomepuyN/BlurEffectForAndroidDesign
+
+public class Blur {
+
+ public static Bitmap apply(Context context, Bitmap sentBitmap) {
+ return apply(context, sentBitmap, 10);
+ }
+
+ @SuppressLint("NewApi")
+ public static Bitmap apply(Context context, Bitmap sentBitmap, float radius) {
+
+ Bitmap bitmap = Bitmap.createScaledBitmap(sentBitmap, sentBitmap.getWidth()/5, sentBitmap.getHeight()/5, false);
+
+ if (VERSION.SDK_INT > 16) {
+ final RenderScript rs = RenderScript.create(context);
+ final Allocation input = Allocation.createFromBitmap(rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
+ Allocation.USAGE_SCRIPT);
+ final Allocation output = Allocation.createTyped(rs, input.getType());
+ final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
+ script.setRadius(radius);
+ script.setInput(input);
+ script.forEach(output);
+ output.copyTo(bitmap);
+
+ return bitmap;
+ }
+
+ // Stack Blur v1.0 from
+ // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ //
+ // Java Author: Mario Klingemann
+ // http://incubator.quasimondo.com
+ // created Feburary 29, 2004
+ // Android port : Yahel Bouaziz
+ // http://www.kayenko.com
+ // ported april 5th, 2012
+
+ // This is a compromise between Gaussian Blur and Box blur
+ // It creates much better looking blurs than Box Blur, but is
+ // 7x faster than my Gaussian Blur implementation.
+ //
+ // I called it Stack Blur because this describes best how this
+ // filter works internally: it creates a kind of moving stack
+ // of colors whilst scanning through the image. Thereby it
+ // just has to add one new block of color to the right side
+ // of the stack and remove the leftmost color. The remaining
+ // colors on the topmost layer of the stack are either added on
+ // or reduced by one, depending on if they are on the right or
+ // on the left side of the stack.
+ //
+ // If you are using this algorithm in your code please add
+ // the following line:
+ //
+ // Stack Blur Algorithm by Mario Klingemann
+
+ if (radius < 1) {
+ return (null);
+ }
+ int intRadius = Math.round(radius);
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ int[] pix = new int[w * h];
+ bitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ int wm = w - 1;
+ int hm = h - 1;
+ int wh = w * h;
+ int div = intRadius + intRadius + 1;
+
+ int r[] = new int[wh];
+ int g[] = new int[wh];
+ int b[] = new int[wh];
+ int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
+ int vmin[] = new int[Math.max(w, h)];
+
+ int divsum = (div + 1) >> 1;
+ divsum *= divsum;
+ int dv[] = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = (i / divsum);
+ }
+
+ yw = yi = 0;
+
+ int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ int r1 = intRadius + 1;
+ int routsum, goutsum, boutsum;
+ int rinsum, ginsum, binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -intRadius; i <= intRadius; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + intRadius];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = intRadius;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - intRadius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + intRadius + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[(stackpointer) % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -intRadius * w;
+ for (i = -intRadius; i <= radius; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + intRadius];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = intRadius;
+ for (y = 0; y < h; y++) {
+ // Preserve alpha channel: ( 0xff000000 & pix[yi] )
+ pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - intRadius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+
+ bitmap.setPixels(pix, 0, w, 0, 0, w, h);
+ return (bitmap);
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/BlurBehind.java b/qbaselib/src/main/java/com/quseit/util/BlurBehind.java
new file mode 100644
index 00000000..b751bd88
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/BlurBehind.java
@@ -0,0 +1,123 @@
+package com.quseit.util;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.AsyncTask;
+import android.util.LruCache;
+import android.view.View;
+
+
+public class BlurBehind {
+
+ private static final String KEY_CACHE_BLURRED_BACKGROUND_IMAGE = "KEY_CACHE_BLURRED_BACKGROUND_IMAGE";
+ private static final int CONSTANT_BLUR_RADIUS = 12;
+ private static final int CONSTANT_DEFAULT_ALPHA = 100;
+
+ private static final LruCache mImageCache = new LruCache(1);
+ private static CacheBlurBehindAndExecuteTask cacheBlurBehindAndExecuteTask;
+
+ private int mAlpha = CONSTANT_DEFAULT_ALPHA;
+ private int mFilterColor = -1;
+
+ private enum State {
+ READY,
+ EXECUTING
+ }
+
+ private State mState = State.READY;
+
+ private static BlurBehind mInstance;
+
+ public static BlurBehind getInstance() {
+ if (mInstance == null) {
+ mInstance = new BlurBehind();
+ }
+ return mInstance;
+ }
+
+ public void execute(Activity activity, OnBlurCompleteListener onBlurCompleteListener) {
+ if (mState.equals(State.READY)) {
+ mState = State.EXECUTING;
+ cacheBlurBehindAndExecuteTask = new CacheBlurBehindAndExecuteTask(activity, onBlurCompleteListener);
+ cacheBlurBehindAndExecuteTask.execute();
+ }
+ }
+
+ public BlurBehind withAlpha(int alpha) {
+ this.mAlpha = alpha;
+ return this;
+ }
+
+ public BlurBehind withFilterColor(int filterColor) {
+ this.mFilterColor = filterColor;
+ return this;
+ }
+
+ public void setBackground(Activity activity) {
+ if (mImageCache.size() != 0) {
+ BitmapDrawable bd = new BitmapDrawable(activity.getResources(), mImageCache.get(KEY_CACHE_BLURRED_BACKGROUND_IMAGE));
+// bd.setAlpha(mAlpha);
+ if (mFilterColor != -1) {
+ bd.setColorFilter(mFilterColor, PorterDuff.Mode.DST_ATOP);
+ }
+ activity.getWindow().setBackgroundDrawable(bd);
+ mImageCache.remove(KEY_CACHE_BLURRED_BACKGROUND_IMAGE);
+ cacheBlurBehindAndExecuteTask = null;
+ }
+ }
+
+ private class CacheBlurBehindAndExecuteTask extends AsyncTask {
+ private Activity activity;
+ private OnBlurCompleteListener onBlurCompleteListener;
+
+ private View decorView;
+ private Bitmap image;
+
+ public CacheBlurBehindAndExecuteTask(Activity activity, OnBlurCompleteListener onBlurCompleteListener) {
+ this.activity = activity;
+ this.onBlurCompleteListener = onBlurCompleteListener;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+
+ decorView = activity.getWindow().getDecorView();
+ decorView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_LOW);
+ decorView.setDrawingCacheEnabled(true);
+ decorView.buildDrawingCache();
+
+ image = decorView.getDrawingCache();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ Bitmap blurredBitmap = Blur.apply(activity, image, CONSTANT_BLUR_RADIUS);
+ mImageCache.put(KEY_CACHE_BLURRED_BACKGROUND_IMAGE, blurredBitmap);
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+
+ decorView.destroyDrawingCache();
+ decorView.setDrawingCacheEnabled(false);
+
+ activity = null;
+
+ onBlurCompleteListener.onBlurComplete();
+
+ mState = State.READY;
+ }
+ }
+
+ public interface OnBlurCompleteListener {
+
+ public void onBlurComplete();
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/DateTimeHelper.java b/qbaselib/src/main/java/com/quseit/util/DateTimeHelper.java
new file mode 100644
index 00000000..a07401aa
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/DateTimeHelper.java
@@ -0,0 +1,297 @@
+package com.quseit.util;
+
+import android.util.Log;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+public class DateTimeHelper {
+ // Wed Dec 15 02:53:36 +0000 2010
+ public static final DateFormat TWITTER_DATE_FORMATTER = new SimpleDateFormat(
+ "E MMM d HH:mm:ss Z yyyy", Locale.US);
+ public static final DateFormat TWITTER_SEARCH_API_DATE_FORMATTER = new SimpleDateFormat(
+ "E, d MMM yyyy HH:mm:ss Z", Locale.US);
+ public static final DateFormat AGO_FULL_DATE_FORMATTER = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss");
+ private static final String TAG = "DateTimeHelper";
+
+ public static String converTime(long timestamp, String[] timeLabels) {
+ long currentSeconds = System.currentTimeMillis() / 1000;
+ long timeGap = currentSeconds - timestamp;//与现在时间相差秒数
+ //Log.d(TAG, "converTime:"+currentSeconds+"-"+timestamp);
+ String timeStr = null;
+ if (timeGap > 24 * 60 * 60 * 30) {
+ //*月前
+ timeStr = timeGap / (24 * 60 * 60) + timeLabels[0];
+ } else if (timeGap > 24 * 60 * 60) {
+ //1天以上
+ timeStr = timeGap / (24 * 60 * 60) + timeLabels[1];
+ } else if (timeGap > 60 * 60) {
+ //1小时-24小时
+ timeStr = timeGap / (60 * 60) + timeLabels[2];
+ } else if (timeGap > 60) {
+ //1分钟-59分钟
+ timeStr = timeGap / 60 + timeLabels[3];
+ } else {
+ //1秒钟-59秒钟
+ timeStr = timeGap + timeLabels[4];
+ }
+ return timeStr;
+ }
+
+ /*public static String getStandardTime(long timestamp) {
+ SimpleDateFormat sdf = new SimpleDateFormat("MM月dd日 HH:mm");
+ Date date = new Date(timestamp * 1000);
+ sdf.format(date);
+ return sdf.format(date);
+ }
+
+ public static String getStandardTime(long timestamp, String format) {
+ SimpleDateFormat sdf = new SimpleDateFormat(format);
+ Date date = new Date(timestamp * 1000);
+ sdf.format(date);
+ return sdf.format(date);
+ }*/
+
+ public static final String getDateAsDirName() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+ String cdate = sdf.format(cal.getTime());
+ return cdate;
+ }
+
+ public static final String getDateAss() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-HH-mm");
+ String cdate = sdf.format(cal.getTime());
+ return cdate;
+ }
+
+ public static final String getDate() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String cdate = sdf.format(cal.getTime());
+ return cdate;
+ }
+
+ public static final String getDateMin() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ String cdate = sdf.format(cal.getTime());
+ return cdate;
+ }
+
+ public static final String getTodayFull() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd 00:00:00");
+ String cdate = sdf.format(cal.getTime());
+ //Log.d(TAG, "getTodayFull:"+cdate);
+ return cdate;
+ }
+
+ public static final int getTimeAsInt() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("HHmmss");
+ String cdate = sdf.format(cal.getTime());
+ return Integer.parseInt(cdate);
+ }
+
+ public static final String getDateAsF() {
+ Calendar cal = Calendar.getInstance();
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
+ String cdate = sdf.format(cal.getTime());
+ return cdate;
+ }
+
+ public static String formatData(Date date, String format) {
+ java.text.SimpleDateFormat sdf = new SimpleDateFormat(format);
+ String cdate = sdf.format(date.getTime());
+ return cdate;
+ }
+
+ public static final Date parseDateTime(String dateString) {
+ try {
+ Log.v(TAG, String.format("in parseDateTime, dateString=%s",
+ dateString));
+ return TWITTER_DATE_FORMATTER.parse(dateString);
+ } catch (ParseException e) {
+ Log.w(TAG, "Could not parse Twitter date string: " + dateString);
+ return null;
+ }
+ }
+
+ public static final Date parseSearchApiDateTime(String dateString) {
+ try {
+ return TWITTER_SEARCH_API_DATE_FORMATTER.parse(dateString);
+ } catch (ParseException e) {
+ Log.w(TAG, "Could not parse Twitter search date string: "
+ + dateString);
+ return null;
+ }
+ }
+
+ /*public static String getRelativeDate(Date date) {
+ Date now = new Date();
+
+ String prefix = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_prefix);
+ String sec = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_sec);
+ String min = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_min);
+ String hour = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_hour);
+ String day = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_day);
+ String suffix = PassingerApp.mContext
+ .getString(R.string.tweet_created_at_beautify_suffix);
+
+ // Seconds.
+ long diff = (now.getTime() - date.getTime()) / 1000;
+
+ if (diff < 0) {
+ diff = 0;
+ }
+
+ if (diff < 60) {
+ return diff + sec + suffix;
+ }
+
+ // Minutes.
+ diff /= 60;
+
+ if (diff < 60) {
+ return prefix + diff + min + suffix;
+ }
+
+ // Hours.
+ diff /= 60;
+
+ if (diff < 24) {
+ return prefix + diff + hour + suffix;
+ }
+
+ return AGO_FULL_DATE_FORMATTER.format(date);
+ }*/
+
+ public static long getNowTime() {
+ return Calendar.getInstance().getTime().getTime();
+ }
+
+ public static long getDiffMin(String date1, String date2) {
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ //java.util.Date now;
+ java.util.Date begin;
+ try {
+ begin = df.parse(date1);
+ java.util.Date end = df.parse(date2);
+ //Log.d(TAG, date1+"-"+date2);
+ long between = (end.getTime() - begin.getTime()) / 1000;//除以1000是为了转换成秒
+
+ long minute1 = between % 3600 / 60;
+
+ return minute1;
+ } catch (ParseException e) {
+ e.printStackTrace();
+
+ return 0;
+ }
+
+ }
+
+ public static String getDiff(Date begin, Date end) {
+ //long between=(end.getTime()-begin.getTime())/1000;//除以1000是为了转换成秒
+
+ //long day1=between/(24*3600);
+ int day2 = (end.getMonth() * 100 + end.getDate()) - (begin.getMonth() * 100 + begin.getDate());
+
+ //long hour1=between%(24*3600)/3600;
+ //long minute1=between%3600/60;
+ //long second1=between%60/60;
+ //String dateAt = String.valueOf(begin.getYear())+BASE_CONF.T_Y+String.valueOf(begin.getMonth())+BASE_CONF.T_M+String.valueOf(begin.getDay())+BASE_CONF.T_D;
+ String x1 = String.valueOf(begin.getHours());
+ if (x1.length() == 1) {
+ x1 = "0" + x1;
+ }
+ String x2 = String.valueOf(begin.getMinutes());
+ if (x2.length() == 1) {
+ x2 = "0" + x2;
+ }
+ String timeAt = x1 + ":" + x2;
+ //String ret = "";
+ //Log.d(TAG, "getDiff:"+date1+"-"+date2+"-"+(end.getMonth()*100+end.getDate())+"-"+(begin.getMonth()*100+begin.getDate()));
+
+ SimpleDateFormat ymformatter = new SimpleDateFormat("yyyyMM");
+ String bYm = ymformatter.format(begin);
+ String eYm = ymformatter.format(end);
+
+ if (bYm.equals(eYm)) {
+ if (day2 > 1) {
+ SimpleDateFormat formatter = new SimpleDateFormat("MM-dd");
+ return formatter.format(begin) + " " + timeAt;
+ }
+
+ if (day2 == 1) {
+ return "Yesterday " + timeAt;
+ }
+
+ return "Today " + timeAt;
+
+
+ } else {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ return formatter.format(begin) + " " + timeAt;
+ }
+
+ }
+
+ public static String getDiff(String date1, String date2) {
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ //java.util.Date now;
+ try {
+ java.util.Date begin = df.parse(date1);
+ java.util.Date end = df.parse(date2);
+ return DateTimeHelper.getDiff(begin, end);
+
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return "unknow";
+ }
+
+ }
+
+ public static String longTimeToString(int i) {
+ StringBuilder sb = new StringBuilder();
+ i = i / 1000;
+ int m = i / 60;
+ int s = i % 60;
+ if (i >= 60) {
+ if (m < 10) {
+ sb.append("0");
+ sb.append(String.valueOf(m));
+ } else {
+ sb.append(String.valueOf(m));
+ }
+ sb.append(":");
+ if (s > 9) {
+ sb.append(String.valueOf(s));
+ } else {
+ sb.append("0");
+ sb.append(String.valueOf(s));
+ }
+ } else {
+ sb.append("00:");
+ if (s > 9) {
+ sb.append(String.valueOf(s));
+ } else {
+ sb.append("0");
+ sb.append(String.valueOf(s));
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/DirTraversal.java b/qbaselib/src/main/java/com/quseit/util/DirTraversal.java
new file mode 100644
index 00000000..707c94ac
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/DirTraversal.java
@@ -0,0 +1,123 @@
+package com.quseit.util;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * 文件夹遍历
+ * @author miaowei
+ *
+ */
+public class DirTraversal {
+
+ //no recursion
+ public static LinkedList listLinkedFiles(String strPath) {
+ LinkedList list = new LinkedList();
+ File dir = new File(strPath);
+ File file[] = dir.listFiles();
+ for (int i = 0; i < file.length; i++) {
+ /*if (file[i].isDirectory()){
+
+ list.add(file[i]);
+ }else{
+
+ System.out.println(file[i].getAbsolutePath());
+
+ }*/
+ list.add(file[i]);
+ }
+ /*File tmp;
+ while (!list.isEmpty()) {
+ tmp = (File) list.removeFirst();
+ if (tmp.isDirectory()) {
+ file = tmp.listFiles();
+ if (file == null)
+ continue;
+ for (int i = 0; i < file.length; i++) {
+ if (file[i].isDirectory())
+ list.add(file[i]);
+ else
+ System.out.println(file[i].getAbsolutePath());
+ }
+ } else {
+ System.out.println(tmp.getAbsolutePath());
+ }
+ }*/
+ return list;
+ }
+
+
+ //recursion
+ public static ArrayList listFiles(String strPath) {
+ return refreshFileList(strPath);
+ }
+
+ public static ArrayList refreshFileList(String strPath) {
+ ArrayList filelist = new ArrayList();
+ File dir = new File(strPath);
+ File[] files = dir.listFiles();
+
+ if (files == null)
+ return null;
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].isDirectory()) {
+ refreshFileList(files[i].getAbsolutePath());
+ } else {
+ if(files[i].getName().toLowerCase().endsWith("zip")){
+
+ filelist.add(files[i]);
+ }
+
+ }
+ }
+ return filelist;
+ }
+
+ public static ArrayList arrayListFiles(String strPath){
+
+ ArrayList filelist = new ArrayList();
+ File dir = new File(strPath);
+ File[] files = dir.listFiles();
+ for (int i = 0; i < files.length; i++) {
+
+ filelist.add(files[i].getAbsoluteFile());
+ }
+ return filelist;
+ }
+ //-----4.0读取文件的报 open failed: ENOENT (No such file or directory)
+ /**
+ * 1\可先创建文件的路径
+ * @param filePath
+ */
+ public static void makeRootDirectory(String filePath) {
+ File file = null;
+ try {
+ file = new File(filePath);
+ if (!file.exists()) {
+ file.mkdir();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ /**
+ * 2\然后在创建文件名就不会在报该错误
+ * @param filePath
+ * @param fileName
+ * @return
+ */
+ public static File getFilePath(String filePath, String fileName) {
+ File file = null;
+ makeRootDirectory(filePath);
+ try {
+ file = new File(filePath + fileName);
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return file;
+ }
+
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/FileHelper.java b/qbaselib/src/main/java/com/quseit/util/FileHelper.java
new file mode 100644
index 00000000..826c3f14
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/FileHelper.java
@@ -0,0 +1,416 @@
+package com.quseit.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.webkit.MimeTypeMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 对SD卡文件的管理
+ *
+ * @author ch.linghu
+ */
+public class FileHelper {
+ @SuppressWarnings("unused")
+ private static final String TAG = "FileHelper";
+ private static List typeFiles;
+
+ public static final void createDirIfNExists(String dirname) {
+ File yy = new File(dirname);
+ if (!yy.exists()) {
+ yy.mkdirs();
+ }
+ }
+
+ public static final void createFileFromAssetsIfNExists(Context con, String filename, String dst) {
+ File yy = new File(dst);
+ if (!yy.exists()) {
+ String content = FileHelper.LoadDataFromAssets(con, filename);
+ FileHelper.writeToFile(dst, content);
+ }
+ }
+
+ public static void openFile(Context context, String filePath, String fileExtension) {
+ Intent intent = new Intent();
+ intent.setAction(android.content.Intent.ACTION_VIEW);
+ File file = new File(filePath);
+ MimeTypeMap mime = MimeTypeMap.getSingleton();
+ String type = mime.getMimeTypeFromExtension(fileExtension);
+ intent.setDataAndType(Uri.fromFile(file), type);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ context.startActivity(intent);
+ } catch (android.content.ActivityNotFoundException e) {
+ }
+ }
+
+ public static String getFileNameFromUrl(String urlFile) {
+ try {
+ URL url = new URL(urlFile);
+ File f = new File(url.getPath());
+ return f.getName();
+
+ } catch (MalformedURLException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+
+ return "unname.dat";
+ }
+ }
+
+ public static String getTypeByMimeType(String mType) {
+ if (mType.equals("application/vnd.android.package-archive")) {
+ return "apk";
+ } else {
+ String[] xx = mType.split("/");
+ if (xx.length > 1) {
+ return xx[0];
+ }
+ }
+ return "other";
+ }
+
+ public static String LoadDataFromAssets(Context context, String inFile) {
+ String tContents = "";
+
+ try {
+ InputStream stream = context.getAssets().open(inFile);
+ int size = stream.available();
+ byte[] buffer = new byte[size];
+ stream.read(buffer);
+ stream.close();
+ tContents = new String(buffer);
+ } catch (IOException e) {
+ }
+ return tContents;
+ }
+
+ public static void putFileContents(String filename, String content) {
+ try {
+ File fileCache = new File(filename);
+ byte[] data = content.getBytes();
+ FileOutputStream outStream;
+ outStream = new FileOutputStream(fileCache);
+ outStream.write(data);
+ outStream.close();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+
+ }
+ }
+
+ public static void writeToFile(String filePath, String data) {
+ try {
+ File file = new File(filePath);
+ if (!file.exists()) {
+ if (!file.createNewFile()) {
+ return;
+ }
+ }
+
+ FileOutputStream fOut = new FileOutputStream(filePath);
+ fOut.write(data.getBytes());
+ fOut.flush();
+ fOut.close();
+ } catch (IOException iox) {
+ iox.printStackTrace();
+ }
+
+ }
+
+ public static String getFileContents(String filename, int pos) {
+
+ File scriptFile = new File(filename);
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ while ((line = in.readLine()) != null) {
+ tContent.append(line).append("\n");
+ if (tContent.length() >= pos) {
+ in.close();
+ return tContent.toString();
+ }
+ }
+ in.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ return tContent.toString();
+ }
+
+ public static String getFileContent(String filename) {
+ File scriptFile = new File(filename);
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ int off = 0;int len = 0;
+ char[] buf = new char[100];
+ while ((len = in.read(buf,off,100)) == 100) {
+ tContent.append(buf);
+ }
+ tContent.append(buf,0,len);
+ in.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ return tContent.toString();
+ }
+
+ public static String getFileContents(String filename) {
+ File scriptFile = new File(filename);
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ while ((line = in.readLine()) != null) {
+ tContent.append(line).append("\n");
+ }
+ in.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ return tContent.toString();
+ }
+
+ public static String getFileContents(File scriptFile) {
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ while ((line = in.readLine()) != null) {
+ tContent.append(line).append("\n");
+ }
+ in.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ return tContent.toString();
+ }
+
+ public static void clearDir(String dir, int level, boolean deleteS) {
+ //Log.d(TAG, "clearDir:"+dir);
+ File basePath = new File(dir);
+ if (basePath.exists() && basePath.isDirectory()) {
+ for (File item : basePath.listFiles()) {
+ if (item.isFile()) {
+ //Log.d(TAG, "deleteItem:"+item.getAbsolutePath());
+ item.delete();
+
+ } else if (item.isDirectory()) {
+ clearDir(item.getAbsolutePath(), level + 1, deleteS);
+ }
+ }
+ if (level > 0 || deleteS) {
+ basePath.delete();
+ }
+ } else if (basePath.exists()) {
+ basePath.delete();
+ }
+ }
+
+ public static File getBasePath(String parDir, String subdir) throws IOException {
+ try {
+ File basePath = new File(Environment.getExternalStorageDirectory(),
+ parDir);
+
+ if (!basePath.exists()) {
+ if (!basePath.mkdirs()) {
+ throw new IOException(String.format("%s cannot be created!",
+ basePath.toString()));
+ }
+ }
+ File subPath = null;
+ if (!subdir.equals("")) {
+ subPath = new File(Environment.getExternalStorageDirectory(),
+ parDir + "/" + subdir);
+ if (!subPath.exists()) {
+ if (!subPath.mkdirs()) {
+ throw new IOException(String.format("%s cannot be created!",
+ subPath.toString()));
+ }
+ }
+ }
+
+ if (!basePath.isDirectory()) {
+ throw new IOException(String.format("%s is not a directory!",
+ basePath.toString()));
+ }
+ if (subdir.equals(""))
+ return basePath;
+ else
+ return subPath;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static File getABSPath(String subdir) throws IOException {
+ File basePath = new File(subdir);
+
+ if (!basePath.exists()) {
+ if (!basePath.mkdirs()) {
+ throw new IOException(String.format("%s cannot be created!",
+ basePath.toString()));
+ }
+ }
+ File subPath = null;
+ if (!subdir.equals("")) {
+ subPath = new File(subdir);
+ if (!subPath.exists()) {
+ if (!subPath.mkdirs()) {
+ throw new IOException(String.format("%s cannot be created!",
+ subPath.toString()));
+ }
+ }
+ }
+
+ if (!basePath.isDirectory()) {
+ throw new IOException(String.format("%s is not a directory!",
+ basePath.toString()));
+ }
+ if (subdir.equals(""))
+ return basePath;
+ else
+ return subPath;
+ }
+
+ public static String getFileName(String filename) {
+ File f = new File(filename);
+ return f.getName();
+ }
+
+ public static String getExt(String filename, String def) {
+ String[] yy = filename.split("\\?");
+ String[] xx = yy[0].split("\\.");
+ //Log.d(TAG, "filename:"+filename+"-size:"+xx.length);
+
+ if (xx.length < 2) {
+ return def;
+ } else {
+ String ext = xx[xx.length - 1];
+ //Log.d(TAG, "ext:"+ext);
+ return ext;
+ }
+ }
+
+ public static JSONObject getUrlAsJO(String link) {
+ try {
+ // get URL content
+ URL url = new URL(link);
+ URLConnection conn = url.openConnection();
+
+ // open the stream and put it into BufferedReader
+ BufferedReader br = new BufferedReader(
+ new InputStreamReader(conn.getInputStream()));
+
+ String inputLine;
+
+
+ String ret = "";
+
+ while ((inputLine = br.readLine()) != null) {
+ ret = ret + inputLine + "\n";
+ }
+
+ br.close();
+
+ try {
+ return new JSONObject(ret.trim());
+ } catch (JSONException e) {
+ return null;
+ }
+ //System.out.println("Done");
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+
+ }
+
+ /**
+ * @param dir
+ * @return The main file to be found in dir
+ */
+ public static File getMainFileByType(File dir) {
+ File xx = new File(dir.getAbsolutePath() + "/main.py");
+ return xx.exists() ? xx : null;
+ }
+
+ /**
+ * Filter Files by type
+ *
+ * @param dir
+ * @return
+ */
+ public static File[] getPyFiles(File dir) {
+ if (dir==null) {
+ return null;
+ }
+ typeFiles = new ArrayList<>();
+ addPyFile(dir);
+ return typeFiles.toArray(new File[0]);
+ }
+
+ private static void addPyFile(File dir) {
+ File[] dirFiles = dir.listFiles();
+ if (dirFiles!=null) {
+ for (File file : dirFiles) {
+ if (file.isDirectory() && !file.getAbsolutePath().contains("/.")) {
+ addPyFile(file);
+ } else {
+ String filename = file.getName();
+ if ((filename.endsWith(".py")||filename.endsWith(".ipynb")) && filename.charAt(0)!='.')
+ typeFiles.add(file);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/FolderUtils.java b/qbaselib/src/main/java/com/quseit/util/FolderUtils.java
new file mode 100644
index 00000000..1d179dc6
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/FolderUtils.java
@@ -0,0 +1,53 @@
+package com.quseit.util;
+
+import java.io.File;
+import java.util.Comparator;
+
+public class FolderUtils {
+ public static final Comparator sortTypeByName = new Comparator() {
+ @Override
+ public int compare(File arg00, File arg11) {
+ String arg0 = arg00.toString();
+ String arg1 = arg11.toString();
+ String ext = null;
+ String ext2 = null;
+ int ret;
+
+ try {
+ ext = arg0.substring(arg0.lastIndexOf(".") + 1, arg0.length()).toLowerCase();
+ ext2 = arg1.substring(arg1.lastIndexOf(".") + 1, arg1.length()).toLowerCase();
+ } catch (IndexOutOfBoundsException e) {
+ return 0;
+ }
+ ret = ext.compareTo(ext2);
+
+ if (ret == 0)
+ return arg0.toLowerCase().compareTo(arg1.toLowerCase());
+ return ret;
+ }
+ };
+
+ public static final Comparator sortByName = new Comparator() {
+ @Override
+ public int compare(File o1, File o2) {
+ String arg0 = o1.getName();
+ String arg1 = o2.getName();
+ String ext;
+ String ext2;
+ int ret;
+
+ try {
+ ext = arg0.substring(arg0.lastIndexOf(".") + 1, arg0.length()).toLowerCase();
+ ext2 = arg1.substring(arg1.lastIndexOf(".") + 1, arg1.length()).toLowerCase();
+
+ } catch (IndexOutOfBoundsException e) {
+ return 0;
+ }
+ ret = ext.compareTo(ext2);
+
+ if (ret == 0)
+ return arg0.toLowerCase().compareTo(arg1.toLowerCase());
+ return ret;
+ }
+ };
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ImageDownLoader.java b/qbaselib/src/main/java/com/quseit/util/ImageDownLoader.java
new file mode 100644
index 00000000..b0ddbd70
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ImageDownLoader.java
@@ -0,0 +1,227 @@
+package com.quseit.util;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.util.LruCache;
+import android.widget.ImageView;
+
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.CoreProtocolPNames;
+import org.apache.http.util.EntityUtils;
+import org.apache.http.client.methods.HttpGet;
+
+import java.io.File;
+import java.util.Hashtable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ImageDownLoader {
+ private static final String ImageDownLoader_Log = Utils
+ .makeLogTag(ImageDownLoader.class);
+ private static final String DIR_CACHE = "imagecache";
+ private static final long DIR_CACHE_LIMIT = 10 * 1024 * 1024;
+ private static final int IMAGE_DOWNLOAD_FAIL_TIMES = 2;
+ private Hashtable taskCollection;
+ private LruCache lruCache;
+ private ExecutorService threadPool;
+ private File cacheFileDir;
+
+ @SuppressLint("NewApi")
+ public ImageDownLoader(Context context) {
+ int maxMemory = (int) Runtime.getRuntime().maxMemory();
+ lruCache = new LruCache(maxMemory / 8) {
+ @Override
+ protected int sizeOf(String key, Bitmap value) {
+ return value.getRowBytes() * value.getHeight() / 1024;
+ }
+ };
+ taskCollection = new Hashtable();
+ threadPool = Executors.newFixedThreadPool(10);
+ cacheFileDir = Utils.createFileDir(context, DIR_CACHE);
+ }
+
+ public static void setImageFromUrl(Context context, final ImageView imageView, String url) {
+ ImageDownLoader loader = new ImageDownLoader(context);
+ Bitmap bitmap = loader.getBitmapCache(url);
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ } else {
+ if (loader.getTaskCollection().containsKey(url)) {
+ return;
+ }
+ loader.loadImage(url, imageView.getWidth(), imageView.getHeight(), new AsyncImageLoaderListener() {
+ @Override
+ public void onImageLoader(Bitmap bitmap) {
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ }
+ }
+ });
+ }
+
+ }
+
+ public static void setBlurImageFromUrl(Context context, final ImageView imageView, String url) {
+ ImageDownLoader loader = new ImageDownLoader(context);
+ Bitmap bitmap = loader.getBitmapCache(url);
+ if (bitmap != null) {
+ bitmap = Blur.apply(context, bitmap,1);
+ imageView.setImageBitmap(bitmap);
+ } else {
+ if (loader.getTaskCollection().containsKey(url)) {
+ return;
+ }
+ loader.loadImage(url, imageView.getWidth(), imageView.getHeight(), new AsyncImageLoaderListener() {
+ @Override
+ public void onImageLoader(Bitmap bitmap) {
+ if (bitmap != null) {
+ imageView.setImageBitmap(bitmap);
+ }
+ }
+ });
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private void addLruCache(String key, Bitmap bitmap) {
+ if (getBitmapFromMemCache(key) == null && bitmap != null) {
+ lruCache.put(key, bitmap);
+ }
+ }
+
+ @SuppressLint("NewApi")
+ private Bitmap getBitmapFromMemCache(String key) {
+ return lruCache.get(key);
+ }
+
+ public void loadImage(final String url, final int width, final int height,
+ AsyncImageLoaderListener listener) {
+ Log.i(ImageDownLoader_Log, "loadImage:" + url);
+ final ImageHandler handler = new ImageHandler(listener);
+ Runnable runnable = new Runnable() {
+ @Override
+ public void run() {
+ //Log.i(ImageDownLoader_Log, "loadImage run:" + url);
+ Bitmap bitmap = downloadImage(url, width, height);
+ Message msg = handler.obtainMessage();
+ msg.obj = bitmap;
+ handler.sendMessage(msg);
+ addLruCache(url, bitmap);
+ long cacheFileSize = Utils.getFileSize(cacheFileDir);
+ if (cacheFileSize > DIR_CACHE_LIMIT) {
+ Log.i(ImageDownLoader_Log, cacheFileDir
+ + " size has exceed limit." + cacheFileSize);
+ Utils.delFile(cacheFileDir, false);
+ taskCollection.clear();
+ }
+ String urlKey = url.replaceAll("[^\\w]", "");
+ Utils.savaBitmap(cacheFileDir, urlKey, bitmap);
+ }
+ };
+ taskCollection.put(url, 0);
+ threadPool.execute(runnable);
+ }
+
+ public Bitmap getBitmapCache(String url) {
+ String urlKey = url.replaceAll("[^\\w]", "");
+ if (getBitmapFromMemCache(url) != null) {
+ return getBitmapFromMemCache(url);
+ } else if (Utils.isFileExists(cacheFileDir, urlKey)
+ && Utils.getFileSize(new File(cacheFileDir, urlKey)) > 0) {
+ Bitmap bitmap = BitmapFactory.decodeFile(cacheFileDir.getPath()
+ + File.separator + urlKey);
+ addLruCache(url, bitmap);
+ return bitmap;
+ }
+ return null;
+ }
+
+ private Bitmap downloadImage(String url, int width, int height) {
+ Bitmap bitmap = null;
+ DefaultHttpClient httpClient = new DefaultHttpClient();
+ try {
+ httpClient.getParams().setParameter(
+ CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
+ HttpGet httpGet = new HttpGet(url);
+ HttpResponse httpResponse = httpClient.execute(httpGet);
+ if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
+ HttpEntity entity = httpResponse.getEntity();
+ byte[] byteIn = EntityUtils.toByteArray(entity);
+ BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options();
+ bmpFactoryOptions.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(byteIn, 0, byteIn.length,
+ bmpFactoryOptions);
+
+ Log.d("ImageDownLoader", "downloadImage:" + bmpFactoryOptions.outHeight + "|" + height + "|" + bmpFactoryOptions.outWidth + "|" + width);
+
+ bmpFactoryOptions.inJustDecodeBounds = false;
+ bitmap = BitmapFactory.decodeByteArray(byteIn, 0,
+ byteIn.length, bmpFactoryOptions);
+ }
+ } catch (ClientProtocolException e) {
+ e.printStackTrace();
+ } catch (ConnectTimeoutException e) {
+ e.printStackTrace();
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (httpClient != null && httpClient.getConnectionManager() != null) {
+ httpClient.getConnectionManager().shutdown();
+ }
+ }
+
+ if (taskCollection.get(url) != null) {
+ int times = taskCollection.get(url);
+ if (bitmap == null
+ && times < IMAGE_DOWNLOAD_FAIL_TIMES) {
+ times++;
+ taskCollection.put(url, times);
+ bitmap = downloadImage(url, width, height);
+ Log.i(ImageDownLoader_Log, "Re-download " + url + ":" + times);
+ }
+ }
+ return bitmap;
+ }
+
+ public synchronized void cancelTasks() {
+ if (threadPool != null) {
+ threadPool.shutdownNow();
+ threadPool = null;
+ }
+ }
+
+ public Hashtable getTaskCollection() {
+ return taskCollection;
+ }
+
+ public interface AsyncImageLoaderListener {
+ void onImageLoader(Bitmap bitmap);
+ }
+
+ static class ImageHandler extends Handler {
+
+ private AsyncImageLoaderListener listener;
+
+ public ImageHandler(AsyncImageLoaderListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ super.handleMessage(msg);
+ listener.onImageLoader((Bitmap) msg.obj);
+ }
+ }
+}
\ No newline at end of file
diff --git a/qbaselib/src/main/java/com/quseit/util/ImageHelper.java b/qbaselib/src/main/java/com/quseit/util/ImageHelper.java
new file mode 100644
index 00000000..0015a7cd
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ImageHelper.java
@@ -0,0 +1,246 @@
+package com.quseit.util;
+
+import android.graphics.Bitmap;
+
+/**
+ * Created by Hmei on 2017-05-22.
+ */
+
+public class ImageHelper {
+ /**
+ * Stack Blur v1.0 from
+ * http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html
+ * Java Author: Mario Klingemann
+ * http://incubator.quasimondo.com
+ *
+ * created Feburary 29, 2004
+ * Android port : Yahel Bouaziz
+ * http://www.kayenko.com
+ * ported april 5th, 2012
+ *
+ * This is a compromise between Gaussian Blur and Box blur
+ * It creates much better looking blurs than Box Blur, but is
+ * 7x faster than my Gaussian Blur implementation.
+ *
+ * I called it Stack Blur because this describes best how this
+ * filter works internally: it creates a kind of moving stack
+ * of colors whilst scanning through the image. Thereby it
+ * just has to add one new block of color to the right side
+ * of the stack and remove the leftmost color. The remaining
+ * colors on the topmost layer of the stack are either added on
+ * or reduced by one, depending on if they are on the right or
+ * on the left side of the stack.
+ *
+ * If you are using this algorithm in your code please add
+ * the following line:
+ * Stack Blur Algorithm by Mario Klingemann
+ */
+
+ public static Bitmap fastblur(Bitmap sentBitmap, float scale, int radius) {
+
+ int width = Math.round(sentBitmap.getWidth() * scale);
+ int height = Math.round(sentBitmap.getHeight() * scale);
+ sentBitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false);
+
+ Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
+
+ if (radius < 1) {
+ return (null);
+ }
+
+ int w = bitmap.getWidth();
+ int h = bitmap.getHeight();
+
+ int[] pix = new int[w * h];
+ Log.e("pix", w + " " + h + " " + pix.length);
+ bitmap.getPixels(pix, 0, w, 0, 0, w, h);
+
+ int wm = w - 1;
+ int hm = h - 1;
+ int wh = w * h;
+ int div = radius + radius + 1;
+
+ int r[] = new int[wh];
+ int g[] = new int[wh];
+ int b[] = new int[wh];
+ int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
+ int vmin[] = new int[Math.max(w, h)];
+
+ int divsum = (div + 1) >> 1;
+ divsum *= divsum;
+ int dv[] = new int[256 * divsum];
+ for (i = 0; i < 256 * divsum; i++) {
+ dv[i] = (i / divsum);
+ }
+
+ yw = yi = 0;
+
+ int[][] stack = new int[div][3];
+ int stackpointer;
+ int stackstart;
+ int[] sir;
+ int rbs;
+ int r1 = radius + 1;
+ int routsum, goutsum, boutsum;
+ int rinsum, ginsum, binsum;
+
+ for (y = 0; y < h; y++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ for (i = -radius; i <= radius; i++) {
+ p = pix[yi + Math.min(wm, Math.max(i, 0))];
+ sir = stack[i + radius];
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+ rbs = r1 - Math.abs(i);
+ rsum += sir[0] * rbs;
+ gsum += sir[1] * rbs;
+ bsum += sir[2] * rbs;
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+ }
+ stackpointer = radius;
+
+ for (x = 0; x < w; x++) {
+
+ r[yi] = dv[rsum];
+ g[yi] = dv[gsum];
+ b[yi] = dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (y == 0) {
+ vmin[x] = Math.min(x + radius + 1, wm);
+ }
+ p = pix[yw + vmin[x]];
+
+ sir[0] = (p & 0xff0000) >> 16;
+ sir[1] = (p & 0x00ff00) >> 8;
+ sir[2] = (p & 0x0000ff);
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[(stackpointer) % div];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi++;
+ }
+ yw += w;
+ }
+ for (x = 0; x < w; x++) {
+ rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
+ yp = -radius * w;
+ for (i = -radius; i <= radius; i++) {
+ yi = Math.max(0, yp) + x;
+
+ sir = stack[i + radius];
+
+ sir[0] = r[yi];
+ sir[1] = g[yi];
+ sir[2] = b[yi];
+
+ rbs = r1 - Math.abs(i);
+
+ rsum += r[yi] * rbs;
+ gsum += g[yi] * rbs;
+ bsum += b[yi] * rbs;
+
+ if (i > 0) {
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+ } else {
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+ }
+
+ if (i < hm) {
+ yp += w;
+ }
+ }
+ yi = x;
+ stackpointer = radius;
+ for (y = 0; y < h; y++) {
+ // Preserve alpha channel: ( 0xff000000 & pix[yi] )
+ pix[yi] = ( 0xff000000 & pix[yi] ) | ( dv[rsum] << 16 ) | ( dv[gsum] << 8 ) | dv[bsum];
+
+ rsum -= routsum;
+ gsum -= goutsum;
+ bsum -= boutsum;
+
+ stackstart = stackpointer - radius + div;
+ sir = stack[stackstart % div];
+
+ routsum -= sir[0];
+ goutsum -= sir[1];
+ boutsum -= sir[2];
+
+ if (x == 0) {
+ vmin[y] = Math.min(y + r1, hm) * w;
+ }
+ p = x + vmin[y];
+
+ sir[0] = r[p];
+ sir[1] = g[p];
+ sir[2] = b[p];
+
+ rinsum += sir[0];
+ ginsum += sir[1];
+ binsum += sir[2];
+
+ rsum += rinsum;
+ gsum += ginsum;
+ bsum += binsum;
+
+ stackpointer = (stackpointer + 1) % div;
+ sir = stack[stackpointer];
+
+ routsum += sir[0];
+ goutsum += sir[1];
+ boutsum += sir[2];
+
+ rinsum -= sir[0];
+ ginsum -= sir[1];
+ binsum -= sir[2];
+
+ yi += w;
+ }
+ }
+
+ Log.e("pix", w + " " + h + " " + pix.length);
+ bitmap.setPixels(pix, 0, w, 0, 0, w, h);
+
+ return (bitmap);
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ImageLoader.java b/qbaselib/src/main/java/com/quseit/util/ImageLoader.java
new file mode 100644
index 00000000..c684e6ae
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ImageLoader.java
@@ -0,0 +1,196 @@
+
+package com.quseit.util;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import android.content.Context;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+/**
+ * An ImageLoader asynchronously loads image from a given url. Client may be
+ * notified from the current image loading state using the
+ * {@link ImageLoaderCallback}.
+ *
+ * Note: You normally don't need to use the {@link ImageLoader}
+ * class directly in your application. You'll generally prefer using an
+ * {@link ImageRequest} that takes care of the entire loading process.
+ *
+ *
+ * @author Cyril Mottier
+ */
+public class ImageLoader {
+
+ private static final String LOG_TAG = ImageLoader.class.getSimpleName();
+
+ public static interface ImageLoaderCallback {
+
+ void onImageLoadingStarted(ImageLoader loader);
+
+ void onImageLoadingEnded(ImageLoader loader, Bitmap bitmap);
+
+ void onImageLoadingFailed(ImageLoader loader, Throwable exception);
+ }
+
+ private static final int ON_START = 0x100;
+ private static final int ON_FAIL = 0x101;
+ private static final int ON_END = 0x102;
+
+ private static ExecutorService sExecutor;
+ private static BitmapFactory.Options sDefaultOptions;
+ private static AssetManager sAssetManager;
+
+ public ImageLoader(Context context) {
+ if (sDefaultOptions == null) {
+ sDefaultOptions = new BitmapFactory.Options();
+ sDefaultOptions.inDither = true;
+ sDefaultOptions.inScaled = true;
+ sDefaultOptions.inDensity = DisplayMetrics.DENSITY_MEDIUM;
+ sDefaultOptions.inTargetDensity = context.getResources().getDisplayMetrics().densityDpi;
+ }
+ sAssetManager = context.getAssets();
+ }
+
+// public Future> loadImage(String url, ImageLoaderCallback callback, BitmapFactory.Options mOptions) {
+// return loadImage(url, callback/*, null*/);
+// }
+
+ public Future> loadImage(String url, ImageLoaderCallback callback) {
+ return loadImage(url, callback, /*bitmapProcessor,*/ null);
+ }
+
+ public Future> loadImage(String url, ImageLoaderCallback callback/*, ImageProcessor bitmapProcessor*/, BitmapFactory.Options options) {
+ return sExecutor.submit(new ImageFetcher(url, callback,/* bitmapProcessor,*/ options));
+ }
+
+ private class ImageFetcher implements Runnable {
+
+ private String mUrl;
+ private ImageHandler mHandler;
+// private ImageProcessor mBitmapProcessor;
+ private BitmapFactory.Options mOptions;
+
+ public ImageFetcher(String url, ImageLoaderCallback callback,/* ImageProcessor bitmapProcessor,*/ BitmapFactory.Options options) {
+ mUrl = url;
+ mHandler = new ImageHandler(url, callback);
+// mBitmapProcessor = bitmapProcessor;
+ mOptions = options;
+ }
+
+ public void run() {
+
+ Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+
+ final Handler h = mHandler;
+ Bitmap bitmap = null;
+ Throwable throwable = null;
+
+ h.sendMessage(Message.obtain(h, ON_START));
+
+ try {
+
+ if (TextUtils.isEmpty(mUrl)) {
+ throw new Exception("The given URL cannot be null or empty");
+ }
+
+ InputStream inputStream = null;
+
+ if (mUrl.startsWith("file:///android_asset/")) {
+ inputStream = sAssetManager.open(mUrl.replaceFirst("file:///android_asset/", ""));
+ } else {
+
+ //NAction.userProxy(QBaseApp.getInstance().getContext());
+
+ inputStream = new URL(mUrl).openStream();
+ }
+
+ // TODO Cyril: Use a AndroidHttpClient?
+
+ bitmap = BitmapFactory.decodeStream(inputStream, null, (mOptions == null) ? sDefaultOptions : mOptions);
+
+ if (bitmap!=null) {
+ bitmap = ImageUtil.toRoundCorner(bitmap);
+ }
+// if (mBitmapProcessor != null && bitmap != null) {
+// final Bitmap processedBitmap = mBitmapProcessor.processImage(bitmap);
+// if (processedBitmap != null) {
+// bitmap = processedBitmap;
+// }
+// }
+
+ } catch (Exception e) {
+ // An error occured while retrieving the image
+// if (Config.GD_ERROR_LOGS_ENABLED) {
+// Log.e(LOG_TAG, "Error while fetching image", e);
+// }
+ throwable = e;
+ }
+
+ if (bitmap == null) {
+ if (throwable == null) {
+ // Skia returned a null bitmap ... that's usually because
+ // the given url wasn't pointing to a valid image
+ throwable = new Exception("Skia image decoding failed");
+ }
+ h.sendMessage(Message.obtain(h, ON_FAIL, throwable));
+ } else {
+ h.sendMessage(Message.obtain(h, ON_END, bitmap));
+ }
+ }
+ }
+
+ private class ImageHandler extends Handler {
+
+ private String mUrl;
+ private ImageLoaderCallback mCallback;
+
+ private ImageHandler(String url, ImageLoaderCallback callback) {
+ mUrl = url;
+ mCallback = callback;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+
+ switch (msg.what) {
+
+ case ON_START:
+ if (mCallback != null) {
+ mCallback.onImageLoadingStarted(ImageLoader.this);
+ }
+ break;
+
+ case ON_FAIL:
+ if (mCallback != null) {
+ mCallback.onImageLoadingFailed(ImageLoader.this, (Throwable) msg.obj);
+ }
+ break;
+
+ case ON_END:
+
+ final Bitmap bitmap = (Bitmap) msg.obj;
+// sImageCache.put(mUrl, bitmap);
+
+ if (mCallback != null) {
+ mCallback.onImageLoadingEnded(ImageLoader.this, bitmap);
+ }
+ break;
+
+ default:
+ super.handleMessage(msg);
+ break;
+ }
+ };
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ImageRequest.java b/qbaselib/src/main/java/com/quseit/util/ImageRequest.java
new file mode 100644
index 00000000..f6569ccb
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ImageRequest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 Cyril Mottier (http://www.cyrilmottier.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.
+ */
+package com.quseit.util;
+
+import com.quseit.util.ImageLoader.ImageLoaderCallback;
+
+import java.util.concurrent.Future;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+/**
+ * An {@link ImageRequest} may be used to request an image from the network. The
+ * process of requesting for an image is done in three steps:
+ *
+ * Instantiate a new {@link ImageRequest}
+ * Call {@link #load(Context)} to start loading the image
+ * Listen to loading state changes using a {@link ImageRequestCallback}
+ *
+ *
+ * @author Cyril Mottier
+ */
+public class ImageRequest {
+
+ /**
+ * @author Cyril Mottier
+ */
+ public static interface ImageRequestCallback {
+ void onImageRequestStarted(ImageRequest request);
+
+ void onImageRequestFailed(ImageRequest request, Throwable throwable);
+
+ void onImageRequestEnded(ImageRequest request, Bitmap image);
+
+ void onImageRequestCancelled(ImageRequest request);
+ }
+
+ private static ImageLoader sImageLoader;
+
+ private Future> mFuture;
+ private String mUrl;
+ private ImageRequestCallback mCallback;
+ private BitmapFactory.Options mOptions;
+
+ public ImageRequest(String url, ImageRequestCallback callback) {
+ this(url, callback, null);
+ }
+
+ public ImageRequest(String url, ImageRequestCallback callback, BitmapFactory.Options options) {
+ mUrl = url;
+ mCallback = callback;
+ mOptions = options;
+ }
+
+ public void setImageRequestCallback(ImageRequestCallback callback) {
+ mCallback = callback;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public void load(Context context) {
+ if (mFuture == null) {
+ if (sImageLoader == null) {
+ sImageLoader = new ImageLoader(context);
+ }
+ mFuture = sImageLoader.loadImage(mUrl, new InnerCallback(), mOptions);
+ }
+ }
+
+ public void cancel() {
+ if (!isCancelled()) {
+ // Here we do not want to force the task to be interrupted. Indeed,
+ // it may be useful to keep the result in a cache for a further use
+ mFuture.cancel(false);
+ if (mCallback != null) {
+ mCallback.onImageRequestCancelled(this);
+ }
+ }
+ }
+
+ public final boolean isCancelled() {
+ return mFuture.isCancelled();
+ }
+
+ private class InnerCallback implements ImageLoaderCallback {
+
+ public void onImageLoadingStarted(ImageLoader loader) {
+ if (mCallback != null) {
+ mCallback.onImageRequestStarted(ImageRequest.this);
+ }
+ }
+
+ public void onImageLoadingEnded(ImageLoader loader, Bitmap bitmap) {
+ if (mCallback != null && !isCancelled()) {
+ mCallback.onImageRequestEnded(ImageRequest.this, bitmap);
+ }
+ mFuture = null;
+ }
+
+ public void onImageLoadingFailed(ImageLoader loader, Throwable exception) {
+ if (mCallback != null && !isCancelled()) {
+ mCallback.onImageRequestFailed(ImageRequest.this, exception);
+ }
+ mFuture = null;
+ }
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ImageUtil.java b/qbaselib/src/main/java/com/quseit/util/ImageUtil.java
new file mode 100644
index 00000000..5a2acec6
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ImageUtil.java
@@ -0,0 +1,373 @@
+package com.quseit.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import com.quseit.config.BASE_CONF;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+public class ImageUtil {
+ public static Bitmap resizeBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
+ int originWidth = bitmap.getWidth();
+ int originHeight = bitmap.getHeight();
+
+ // no need to resize
+ if (originWidth < maxWidth && originHeight < maxHeight) {
+ return bitmap;
+ }
+
+ int width = originWidth;
+ int height = originHeight;
+
+ // 若图片过宽, 则保持长宽比缩放图片
+ if (originWidth > maxWidth) {
+ width = maxWidth;
+
+ double i = originWidth * 1.0 / maxWidth;
+ height = (int) Math.floor(originHeight / i);
+
+ bitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
+ }
+
+ // 若图片过长, 则从上端截取
+ if (height > maxHeight) {
+ height = maxHeight;
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height);
+ }
+
+// Log.i(TAG, width + " width");
+// Log.i(TAG, height + " height");
+
+ return bitmap;
+ }
+ /*public static String getImageString(String imgFilePath){
+ Bitmap mBitmap=BitmapFactory.decodeFile(imgFilePath);
+ Matrix matrix = new Matrix();
+ matrix.postScale(0.5f, 0.5f);
+ Bitmap newBitmap=Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
+
+ ByteArrayOutputStream out=new ByteArrayOutputStream();
+ newBitmap.compress(CompressFormat.JPEG, 100, out);
+ byte []bytes=out.toByteArray();
+ String imageString=Base64.encodeToString(bytes, Base64.DEFAULT);
+ return imageString;
+ }*/
+
+ public static Bitmap getBitFromImg(String imgFilePath) {
+ //try {
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+ bitmapOptions.inSampleSize = 1;
+ bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
+
+ Bitmap mBitmap=BitmapFactory.decodeFile(imgFilePath, bitmapOptions);
+ return mBitmap;
+ /*} catch (OutOfMemoryError e) {
+ BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
+
+ bitmapOptions.inJustDecodeBounds = true;
+ //BitmapFactory.decodeFile(imgFilePath, bitmapOptions);
+ bitmapOptions.inSampleSize = computeSampleSize(bitmapOptions, -1, 128*128);
+ //bitmapOptions.inJustDecodeBounds = false;
+
+ //bitmapOptions.inSampleSize = 2;
+
+ try {
+ Bitmap mBitmap=BitmapFactory.decodeFile(imgFilePath, bitmapOptions);
+ return mBitmap;
+ } catch (OutOfMemoryError E){
+ //bitmapOptions.inSampleSize = 4;
+ //Bitmap mBitmap=BitmapFactory.decodeFile(imgFilePath, bitmapOptions);
+ return null;
+ }
+
+ }*/
+ }
+
+ public static InputStream getRequest(String path) throws Exception {
+ URL url = new URL(path);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(5000);
+ if (conn.getResponseCode() == 200){
+ return conn.getInputStream();
+ }
+ return null;
+ }
+
+
+ public static Bitmap getURLAsBitmap(URL url) {
+ try {
+ URLConnection conn = url.openConnection();
+ conn.connect();
+ InputStream isCover = conn.getInputStream();
+ Bitmap bmpCover = BitmapFactory.decodeStream(isCover);
+ isCover.close();
+ return bmpCover;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+ public static String saveBitmap(String imgHashPath, Bitmap image) {
+ byte[] bmpb = ImageUtil.Bitmap2Bytes(image);
+
+ try {
+ File imgCache = new File(imgHashPath);
+ if (!imgCache.exists()) {
+ imgCache.createNewFile();
+ }
+
+ RandomAccessFile accessFile = new RandomAccessFile(imgCache.getAbsoluteFile(), "rwd");
+ accessFile.setLength(bmpb.length);
+ accessFile.seek(0);
+ accessFile.write(bmpb, 0, bmpb.length);
+ accessFile.close();
+ return imgHashPath;
+ } catch (FileNotFoundException e) {
+
+ e.printStackTrace();
+ return "";
+ } catch (IOException e) {
+
+ e.printStackTrace();
+ return "";
+
+ }
+ }
+
+ public static byte[] readInputStream(InputStream inStream) throws Exception {
+ ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int len = 0;
+ while ((len = inStream.read(buffer)) != -1) {
+ outSteam.write(buffer, 0, len);
+ }
+ outSteam.close();
+ inStream.close();
+ return outSteam.toByteArray();
+ }
+
+ public static Drawable loadImageFromUrl(String url){
+ URL m;
+ InputStream i = null;
+ try {
+ m = new URL(url);
+ i = (InputStream) m.getContent();
+ } catch (MalformedURLException e1) {
+ e1.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Drawable d = Drawable.createFromStream(i, "src");
+ return d;
+ }
+
+ public static Drawable getDrawableFromUrl(String url) throws Exception{
+ return Drawable.createFromStream(getRequest(url),null);
+ }
+
+ public static Bitmap getBitmapFromUrl(String url) throws Exception{
+ byte[] bytes = getBytesFromUrl(url);
+ return byteToBitmap(bytes);
+ }
+
+ public static Bitmap getRoundBitmapFromUrl(String url,int pixels) throws Exception{
+ byte[] bytes = getBytesFromUrl(url);
+ Bitmap bitmap = byteToBitmap(bytes);
+ return toRoundCorner(bitmap, pixels);
+ }
+
+ public static Drawable geRoundDrawableFromUrl(String url,int pixels) throws Exception{
+ byte[] bytes = getBytesFromUrl(url);
+ BitmapDrawable bitmapDrawable = (BitmapDrawable)byteToDrawable(bytes);
+ return toRoundCorner(bitmapDrawable, pixels);
+ }
+
+ public static byte[] getBytesFromUrl(String url) throws Exception{
+ return readInputStream(getRequest(url));
+ }
+
+ public static Bitmap byteToBitmap(byte[] byteArray){
+ if(byteArray.length!=0){
+ return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
+ }
+ else {
+ return null;
+ }
+ }
+
+ public static Drawable byteToDrawable(byte[] byteArray){
+ ByteArrayInputStream ins = new ByteArrayInputStream(byteArray);
+ return Drawable.createFromStream(ins, null);
+ }
+
+ public static byte[] Bitmap2Bytes(Bitmap bm){
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
+ return baos.toByteArray();
+ }
+
+ public static Bitmap drawableToBitmap(Drawable drawable) {
+ Bitmap bitmap = Bitmap
+ .createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
+ : Bitmap.Config.RGB_565);
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+
+ /**
+ * 图片去色,返回灰度图片
+ * @param bmpOriginal 传入的图片
+ * @return 去色后的图片
+ */
+ public static Bitmap toGrayscale(Bitmap bmpOriginal) {
+ int width, height;
+ height = bmpOriginal.getHeight();
+ width = bmpOriginal.getWidth();
+
+ Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+ Canvas c = new Canvas(bmpGrayscale);
+ Paint paint = new Paint();
+ ColorMatrix cm = new ColorMatrix();
+ cm.setSaturation(0);
+ ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
+ paint.setColorFilter(f);
+ c.drawBitmap(bmpOriginal, 0, 0, paint);
+ return bmpGrayscale;
+ }
+
+
+ /**
+ * 去色同时加圆角
+ * @param bmpOriginal 原图
+ * @param pixels 圆角弧度
+ * @return 修改后的图片
+ */
+ public static Bitmap toGrayscale(Bitmap bmpOriginal, int pixels) {
+ return toRoundCorner(toGrayscale(bmpOriginal), pixels);
+ }
+
+ /**
+ * 把图片变成圆角
+ * @param bitmap 需要修改的图片
+ * @param pixels 圆角的弧度
+ * @return 圆角图片
+ */
+ public static Bitmap toRoundCorner(Bitmap bitmap) {
+ return ImageUtil.toRoundCorner(bitmap, BASE_CONF.ROUND_PIX);
+ }
+ public static Bitmap toRoundCorner(Bitmap bitmap, int pixels) {
+ if (pixels == 0)
+ return bitmap;
+
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
+ Canvas canvas = new Canvas(output);
+
+ final int color = 0xff424242;
+ final Paint paint = new Paint();
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ final RectF rectF = new RectF(rect);
+ final float roundPx = pixels;
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
+
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+
+ /**
+ * 使圆角功能支持BitampDrawable
+ * @param bitmapDrawable
+ * @param pixels
+ * @return
+ */
+ public static BitmapDrawable toRoundCorner(BitmapDrawable bitmapDrawable, int pixels) {
+ Bitmap bitmap = bitmapDrawable.getBitmap();
+ bitmapDrawable = new BitmapDrawable(toRoundCorner(bitmap, pixels));
+ return bitmapDrawable;
+ }
+
+
+ public static int computeSampleSize(BitmapFactory.Options options,
+ int minSideLength, int maxNumOfPixels) {
+ int initialSize = computeInitialSampleSize(options, minSideLength,maxNumOfPixels);
+
+ int roundedSize;
+ if (initialSize <= 8 ) {
+ roundedSize = 1;
+ while (roundedSize < initialSize) {
+ roundedSize <<= 1;
+ }
+ } else {
+ roundedSize = (initialSize + 7) / 8 * 8;
+ }
+
+ return roundedSize;
+ }
+
+ private static int computeInitialSampleSize(BitmapFactory.Options options,int minSideLength, int maxNumOfPixels) {
+ double w = options.outWidth;
+ double h = options.outHeight;
+
+ int lowerBound = (maxNumOfPixels == -1) ? 1 :
+ (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
+ int upperBound = (minSideLength == -1) ? 128 :
+ (int) Math.min(Math.floor(w / minSideLength),
+ Math.floor(h / minSideLength));
+
+ if (upperBound < lowerBound) {
+ // return the larger one when there is no overlapping zone.
+ return lowerBound;
+ }
+
+ if ((maxNumOfPixels == -1) &&
+ (minSideLength == -1)) {
+ return 1;
+ } else if (minSideLength == -1) {
+ return lowerBound;
+ } else {
+ return upperBound;
+ }
+ }
+
+ public static float dp2px(float dp) {
+ Resources r = Resources.getSystem();
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/KeyboardUtils.java b/qbaselib/src/main/java/com/quseit/util/KeyboardUtils.java
new file mode 100644
index 00000000..ca270434
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/KeyboardUtils.java
@@ -0,0 +1,121 @@
+package com.quseit.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Build;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import java.util.HashMap;
+
+/**
+ * Based on the following Stackoverflow answer:
+ * http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
+ *
+ * Only works when android:windowSoftInputMode="adjustPan"
+ */
+public class KeyboardUtils implements ViewTreeObserver.OnGlobalLayoutListener
+{
+
+ Context context;
+ @Override
+ public void onGlobalLayout()
+ {
+// Rect r = new Rect();
+// //r will be populated with the coordinates of your view that area still visible.
+// mRootView.getWindowVisibleDisplayFrame(r);
+//
+// int heightDiff = mRootView.getRootView().getHeight() - (r.bottom - r.top);
+// float dp = heightDiff/ mScreenDensity;
+//
+// if(mCallback != null)
+// mCallback.onToggleSoftKeyboard(dp > 200);
+ // navigation bar height
+ int navigationBarHeight = 0;
+ int resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ navigationBarHeight = context.getResources().getDimensionPixelSize(resourceId);
+ }
+
+ // status bar height
+ int statusBarHeight = 0;
+ resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
+ }
+
+ // display window size for the app layout
+ Rect rect = new Rect();
+ mRootView.getWindowVisibleDisplayFrame(rect);
+
+ // screen height - (user app height + status + nav) ..... if non-zero, then there is a soft keyboard
+ int keyboardHeight = mRootView.getHeight() - (statusBarHeight + navigationBarHeight + rect.height());
+
+ if (keyboardHeight <= 0) {
+ mCallback.onToggleSoftKeyboard(false);
+ } else {
+ mCallback.onToggleSoftKeyboard(true);
+ }
+ }
+
+ public interface SoftKeyboardToggleListener
+ {
+ void onToggleSoftKeyboard(boolean isVisible);
+ }
+
+ private SoftKeyboardToggleListener mCallback;
+ private View mRootView;
+ private float mScreenDensity = 1;
+ private static HashMap sListenerMap = new HashMap<>();
+
+
+
+ public static void addKeyboardToggleListener(Activity act, SoftKeyboardToggleListener listener)
+ {
+ removeKeyboardToggleListener(listener);
+
+ sListenerMap.put(listener, new KeyboardUtils(act, listener));
+ }
+
+ public static void removeKeyboardToggleListener(SoftKeyboardToggleListener listener)
+ {
+ if(sListenerMap.containsKey(listener))
+ {
+ KeyboardUtils k = sListenerMap.get(listener);
+ k.removeListener();
+
+ sListenerMap.remove(listener);
+ }
+ }
+
+ public static void removeAllKeyboardToggleListeners()
+ {
+ for(SoftKeyboardToggleListener l : sListenerMap.keySet())
+ sListenerMap.get(l).removeListener();
+
+ sListenerMap.clear();
+ }
+
+ private void removeListener()
+ {
+ mCallback = null;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ }
+ }
+
+ private KeyboardUtils(Activity act, SoftKeyboardToggleListener listener)
+ {
+ mCallback = listener;
+ context = act;
+ mRootView = ((ViewGroup) act.findViewById(android.R.id.content)).getChildAt(0);
+ mRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
+ mScreenDensity = act.getResources().getDisplayMetrics().density;
+ }
+
+
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/Log.java b/qbaselib/src/main/java/com/quseit/util/Log.java
new file mode 100644
index 00000000..9271788b
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/Log.java
@@ -0,0 +1,69 @@
+package com.quseit.util;
+
+import java.io.IOException;
+import java.io.Writer;
+
+public class Log {
+ private static final String DEBUG = "DEBUG: ";
+ //private static final String WARN = "WARN: ";
+ //private static final String INFO = "INFO: ";
+ private static final String ERROR = "ERROR: ";
+
+ private static final int MESSAGE_QUEUE_LENGTH = 50;
+ private static String[] mMessages = new String[MESSAGE_QUEUE_LENGTH];
+ private static int mMessageIdx = 0;
+ private static int mMessageCnt = 0;
+ //private static StringBuilder mStringBuilder = new StringBuilder();
+
+ public static void d (String tag, String msg) {
+ android.util.Log.d(tag, msg);
+ log(DEBUG, tag, msg, null);
+ }
+
+ public static void d (String tag, Throwable t) {
+ android.util.Log.d(tag, "", t);
+ log(DEBUG, tag, "", t);
+ }
+
+ public static void d (String tag, String msg, Throwable t) {
+ android.util.Log.d(tag, msg, t);
+ log(DEBUG, tag, msg, t);
+ }
+
+ public static void e (String tag, String msg) {
+ android.util.Log.e(tag, msg);
+ log(ERROR, tag, msg, null);
+ }
+
+ public static void e (String tag, Throwable t) {
+ android.util.Log.e(tag, "", t);
+ log(ERROR, tag, "", t);
+ }
+
+
+ public static void e (String tag, String msg, Throwable t) {
+ android.util.Log.e(tag, msg, t);
+ log(ERROR, tag, msg, t);
+ }
+
+ private static void log(String type, String tag, String msg, Throwable t) {
+ mMessages[mMessageIdx] = type + tag + ";" + msg + ((t != null) ? ";" + t : "");
+
+ mMessageIdx = (mMessageIdx + 1) % MESSAGE_QUEUE_LENGTH;
+ ++mMessageCnt;
+ }
+
+ public static void dump(final Writer w) throws IOException {
+ if (mMessageCnt >= MESSAGE_QUEUE_LENGTH) {
+ for (int i = mMessageIdx; i < MESSAGE_QUEUE_LENGTH; i++) {
+ w.append(mMessages[i] + "\n");
+ }
+ }
+
+ for (int i = 0; i < mMessageIdx; i++) {
+ w.append(mMessages[i] + "\n");
+ }
+
+ w.flush();
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/MD5.java b/qbaselib/src/main/java/com/quseit/util/MD5.java
new file mode 100644
index 00000000..589ccb7c
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/MD5.java
@@ -0,0 +1,55 @@
+package com.quseit.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+//import org.apache.commons.codec.digest.DigestUtils;//开发的jar包 使用更方便
+public class MD5 {
+
+ /*
+ * 1.一个运用基本类的实例
+ * MessageDigest 对象开始被初始化。该对象通过使用 update 方法处理数据。
+ * 任何时候都可以调用 reset 方法重置摘要。
+ * 一旦所有需要更新的数据都已经被更新了,应该调用 digest 方法之一完成哈希计算。
+ * 对于给定数量的更新数据,digest 方法只能被调用一次。
+ * 在调用 digest 之后,MessageDigest 对象被重新设置成其初始状态。
+ */
+ public static String encrypByMd5(String context) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.update(context.getBytes());//update处理
+ byte [] encryContext = md.digest();//调用该方法完成计算
+
+ int i;
+ StringBuffer buf = new StringBuffer("");
+ for (int offset = 0; offset < encryContext.length; offset++) {//做相应的转化(十六进制)
+ i = encryContext[offset];
+ if (i < 0) i += 256;
+ if (i < 16) buf.append("0");
+ buf.append(Integer.toHexString(i));
+ }
+ return buf.toString().substring(8, 24);
+
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /*
+ * 2.使用开发的jar直接应用
+ * 使用外部的jar包中的类:import org.apache.commons.codec.digest.DigestUtils;
+ * 对上面内容的一个封装使用方便
+ */
+ /* public void encrypByMd5Jar(String context) {
+ String md5Str = DigestUtils.md5Hex(context);
+ System.out.println("32result: " + md5Str);
+ }
+
+ public static void main(String[] args) {
+ MD5 md5 = new MD5();
+ md5.encrypByMd5("yang");
+ md5.encrypByMd5Jar("yang");
+ } */
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/NAction.java b/qbaselib/src/main/java/com/quseit/util/NAction.java
new file mode 100644
index 00000000..95a2ed19
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/NAction.java
@@ -0,0 +1,518 @@
+package com.quseit.util;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ConfigurationInfo;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.quseit.android.R;
+import com.quseit.config.BASE_CONF;
+import com.quseit.common.db.UserLog;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.util.Properties;
+import java.util.UUID;
+
+
+public class NAction {
+
+ private static final String TAG = "NAction";
+ // check rooted
+ private final static int kSystemRootStateUnknow=-1;
+ private final static int kSystemRootStateDisable=0;
+ private final static int kSystemRootStateEnable=1;
+ private static int systemRootState=kSystemRootStateUnknow;
+
+ public static Notification getNotification(Context context, String contentTitle, String contentText, PendingIntent intent,
+ int smallIconId, Bitmap largeIconId, int flags) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ Notification notification = new Notification.Builder(context) //new Notification(icon, tickerText, when);
+ .setTicker(contentTitle)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(smallIconId)
+ .setLargeIcon(largeIconId)
+ .setAutoCancel(true)
+ .setContentIntent(intent)
+ .build();
+
+ return notification;
+ } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
+ Notification notification = new Notification.Builder(context) //new Notification(icon, tickerText, when);
+ .setTicker(contentTitle)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(smallIconId)
+ .setSmallIcon(smallIconId)
+ .setLargeIcon(largeIconId)
+ .setAutoCancel(true)
+ .setContentIntent(intent)
+ .getNotification();
+ return notification;
+ } else {
+ Notification notification = new Notification(smallIconId, contentTitle, System.currentTimeMillis());
+ notification.tickerText = contentTitle;
+ notification.contentIntent = intent;
+ notification.flags |= flags;
+ return null;
+ }
+ }
+
+ @SuppressLint("NewApi")
+ public static boolean isOpenGL2supported(Context context) {
+
+ final ActivityManager activityManager =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ final ConfigurationInfo configurationInfo =
+ activityManager.getDeviceConfigurationInfo();
+ final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
+ return supportsEs2;
+ }
+
+ public static void setInstallLink(Context context, String link) {
+ NStorage.setSP(context, "config.installlink", link);
+ }
+
+ public static String getDefaultRoot(Context context) {
+ return NStorage.getSP(context, "config.defaultroot");
+ }
+
+ public static void sendEmail(Context context, String mailto, String title, String body) {
+ Intent intent = new Intent(android.content.Intent.ACTION_SEND);
+ intent.setType("plain/text");
+ String[] strEmailReciver = new String[]{mailto};
+ intent.putExtra(android.content.Intent.EXTRA_EMAIL, strEmailReciver); //设置收件人
+ intent.putExtra(android.content.Intent.EXTRA_SUBJECT, title); //设置主题
+
+ intent.putExtra(android.content.Intent.EXTRA_TEXT, body); //设置内容
+ context.startActivity(Intent.createChooser(intent, context.getResources().getString(R.string.send_email)));
+ }
+
+ public static void setExtConf(Context context, String conf) {
+ NStorage.setSP(context, "config.ext", conf);
+ }
+
+ public static String getExtConf(Context context) {
+ return NStorage.getSP(context, "config.ext");
+ }
+
+ public static void setExtPluginsConf(Context context, String conf) {
+ NStorage.setSP(context, "config.ext_plugins", conf);
+ }
+
+ public static String getExtPluginsConf(Context context) {
+ return NStorage.getSP(context, "config.ext_plugins");
+ }
+
+ public static void setExtAdConf(Context context, String conf) {
+ NStorage.setSP(context, "config.ext_ad", conf);
+ }
+
+ public static String getExtP(Context context, String key) {
+ String conf = NAction.getExtConf(context);
+ if (conf.equals("")) {
+ return "";
+ } else {
+ try {
+ JSONObject a = new JSONObject(conf);
+ return a.getString(key);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ if (BASE_CONF.DEBUG) Log.d(TAG, "getExtP:"+key+"-not found");
+ //e.printStackTrace();
+ return "";
+ }
+ }
+ }
+
+ public static void setUpdateHost(Context context, String host) {
+ NStorage.setSP(context, "service.updatehost", host);
+ }
+
+ public static String getUpdateHost(Context context) {
+ String h = NStorage.getSP(context, "service.updatehost");
+
+ return h;
+ }
+
+
+ public static long getRemoteFileSize(Context context, String downloadUrl, long startPos) {
+ NAction.userProxy(context);
+
+ try {
+ URL url = new URL(downloadUrl);
+ HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
+ httpConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.0.1; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1");
+ //httpConnection.setRequestProperty("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7");
+ //httpConnection.setRequestProperty("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
+ httpConnection.setRequestProperty("RANGE", "bytes=" + startPos + "-");
+
+ httpConnection.setConnectTimeout(30000);
+ httpConnection.setReadTimeout(30000);
+ httpConnection.connect();
+ long fileTotalSize = httpConnection.getContentLength();
+
+ if (httpConnection.getResponseCode() >= 400) {
+ httpConnection.disconnect();
+
+ return -1;
+ }
+ httpConnection.disconnect();
+
+ return fileTotalSize;
+
+ } catch (IOException e) {
+ if (BASE_CONF.DEBUG) Log.d(TAG, "getRemouteSize IOException:"+e.getMessage());
+ e.printStackTrace();
+ }
+ return -1;
+ }
+
+ public static void userProxy(Context context) {
+ String proxyHost = NAction.getProxyHost(context);
+ String proxyPort = NAction.getProxyPort(context);
+ String proxyUsername = NAction.getProxyUsername(context);
+ String proxyPwd = NAction.getProxyPwd(context);
+
+ if (!proxyHost.equals("")) {
+ Properties props = System.getProperties();
+ props.put("http.proxyHost", proxyHost);
+ props.put("http.proxyPort", proxyPort);
+ if (!proxyUsername.equals("")) {
+ props.put("http.proxyUsername", proxyUsername);
+ props.put("http.proxyPassword", proxyPwd);
+
+ }
+ }
+ }
+
+ public static Intent getLinkAsIntent(Context context, String link) {
+ //Log.d(TAG, "openRemoteLink:"+link);
+ String vlowerFileName = link.toLowerCase();
+ if (vlowerFileName.startsWith("lgmarket:")) {
+ String[] xx = link.split(":");
+ //Log.d(TAG, "lgmarket:"+xx[1]);
+
+ Intent intent = new Intent("com.lge.lgworld.intent.action.VIEW");
+ intent.setClassName("com.lge.lgworld", "com.lge.lgworld.LGReceiver");
+ intent.putExtra("lgworld.receiver","LGSW_INVOKE_DETAIL");
+ intent.putExtra("APP_PID", xx[1]);
+
+ /*Intent intent = new Intent();
+ intent.setClassName("com.lg.apps.cubeapp", "com.lg.apps.cubeapp.PreIntroActivity");
+ intent.putExtra("type", "APP_DETAIL ");
+ intent.putExtra("codeValue", ""); // value is not needed when moving to Detail page
+ intent.putExtra("content_id", xx[1]); */
+
+ context.sendBroadcast(intent);
+
+ return null;
+
+ } else {
+ Uri uLink = Uri.parse(link);
+
+ Intent intent = new Intent( Intent.ACTION_VIEW, uLink );
+
+ return intent;
+ }
+ }
+
+ public static String getUserNoId(Context context) {
+ String usernoid = NStorage.getSP(context, "user.usernoid");
+ if (usernoid.equals("")) {
+ // TODO
+ //UUID uuid = UUID.randomUUID();
+ usernoid = UUID.randomUUID().toString();
+ NStorage.setSP(context, "user.usernoid", usernoid);
+ }
+
+ return usernoid;
+ }
+
+
+ public static void recordAdLog(Context context, String act, String key) {
+ if (NAction.getExtP(context, "conf_log_ad_enable").equals("1")) {
+ UserLog pq = new UserLog(context);
+ if (!pq.checkIfLogExists(act, key, "", "13", "")) {
+
+ pq.insertNewLog(act, key, "", "13", "", 0);
+ }
+ //pq.close();
+ }
+ }
+
+ public static int getUpdateQ(Context context) {
+ String seq = NStorage.getSP(context, "app.update_seq");
+ if (BASE_CONF.DEBUG) Log.d(TAG, "getUpdateQ:"+seq);
+ if (seq.equals("")) {
+ return 0;
+ } else {
+ try {
+ return Integer.parseInt(seq);
+ } catch (Exception e) {
+ return 3;
+ }
+ }
+ }
+
+ public static void setAd(Context context, String who, String banner, String link, String key, String term, String act) {
+ NStorage.setSP(context, "ad.who", who);
+ NStorage.setSP(context, "ad.banner", banner);
+ NStorage.setSP(context, "ad.link", link);
+ NStorage.setSP(context, "ad.key", key);
+ NStorage.setSP(context, "ad.term", term);
+ NStorage.setSP(context, "ad.act", act);
+ }
+
+ public static void setProxyPort(Context context, String val) {
+ NStorage.setSP(context, "proxy.port", val);
+ }
+
+ public static String getProxyHost(Context context) {
+ String val = NStorage.getSP(context, "proxy.host");
+ return val;
+ }
+
+ public static String getProxyPort(Context context) {
+ String val = NStorage.getSP(context, "proxy.port");
+ return val;
+ }
+
+ public static String getProxyUsername(Context context) {
+ String val = NStorage.getSP(context, "proxy.username");
+ return val;
+ }
+
+ public static String getProxyPwd(Context context) {
+ String val = NStorage.getSP(context, "proxy.pwd");
+ return val;
+ }
+
+
+ public static int getUpdateCheckTime(Context context) {
+ String s = NStorage.getSP(context, "tmp.update_check_time");
+ if (s.equals("")) {
+ return 0;
+ } else {
+ return Integer.parseInt(s);
+ }
+ }
+
+ public static void setUpdateCheckTime(Context context) {
+ NStorage.setSP(context, "tmp.update_check_time", String.valueOf(VeDate.getStringDateHourAsInt()));
+ }
+
+ public static String getUserName(Context context) {
+ return NStorage.getSP(context, "user.username");
+ }
+
+ public static String getUID(Context context) {
+ return NStorage.getSP(context, "user.uid");
+ }
+
+ public static String getToken(Context context) {
+ return NStorage.getSP(context, "user.token");
+ }
+
+
+ public static String getCode(Context context) {
+ String packageName = context.getPackageName();
+ String[] xcode = packageName.split("\\.");
+ String code = xcode[xcode.length-1];
+ return code;
+ }
+
+ public static String getUserUrl(Context context) {
+ String sdk = "0";
+ try {
+ sdk = Build.VERSION.SDK;
+ } catch (Exception e) {
+
+ }
+ return "uid="+NAction.getUID(context)+"&token="+NAction.getToken(context)+"&userno="+NAction.getUserNoId(context)+"&lang="+NUtil.getLang()+
+ "&ver="+NUtil.getVersionCode(context)+"&code="+NAction.getCode(context)+"&sdk="+sdk+"&appid="+context.getPackageName();
+ }
+
+ // ftp
+ public static void setFtpRoot(Context context, String root) {
+ NStorage.setSP(context, "ftp.root", root);
+ }
+
+ public static String getFtpRoot(Context context) {
+ return NStorage.getSP(context, "ftp.root");
+ }
+
+ public static void setFtpUsername(Context context, String username) {
+ NStorage.setSP(context, "ftp.username", username);
+ }
+
+ public static void setFtpPwd(Context context, String pwd) {
+ NStorage.setSP(context, "ftp.pwd", pwd);
+ }
+
+ public static String getFtpUsername(Context context) {
+ return NStorage.getSP(context, "ftp.username");
+ }
+
+ public static String getFtpPwd(Context context) {
+ return NStorage.getSP(context, "ftp.pwd");
+ }
+
+ public static void setFtpPort(Context context, String port) {
+ NStorage.setSP(context, "ftp.port", port);
+ }
+
+ public static String getFtpPort(Context context) {
+ return NStorage.getSP(context, "ftp.port");
+ }
+
+
+ public static boolean isQPy3() {
+ return true;
+ }
+
+ public static boolean isQPyInterpreterSet(Context context) {
+ String qpyInterVal = NStorage.getSP(context, "conf.default_qpy_interpreter");
+ return !qpyInterVal.equals("");
+ }
+ public static String getQPyInterpreter(Context context) {
+ return "3.x";
+ /*String qpyInterVal = NStorage.getSP(context, "conf.default_qpy_interpreter");
+ if (!qpyInterVal.startsWith("3.")) {
+ qpyInterVal = "2.x";
+ }
+
+ return qpyInterVal;*/
+ }
+
+ public static void setQPyInterpreter(Context context, String qpyInterVal) {
+ NStorage.setSP(context, "conf.default_qpy_interpreter", qpyInterVal);
+ // It shouldn't be here and need to be refactor
+ try {
+ ACache.get(context).clear();
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ }
+
+ public static boolean httpPing(String url, int timeout) {
+ //Log.d(TAG, "httpPing:"+url+"-"+timeout);
+ url = url.replaceFirst("https", "http"); // Otherwise an exception may be thrown on invalid SSL certificates.
+
+ try {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(timeout);
+ connection.setReadTimeout(timeout);
+ connection.setRequestMethod("HEAD");
+ int responseCode = connection.getResponseCode();
+ //Log.d(TAG, "responseCode:"+responseCode);
+ return (responseCode>0);
+ //return (200 <= responseCode && responseCode <= 399);
+ } catch (IOException exception) {
+ Log.d(TAG, "exception:"+exception.getLocalizedMessage());
+
+ return false;
+ }
+ }
+
+ static public boolean portIsOpen(String ip, int port, int timeout) {
+ try {
+ Socket socket = new Socket();
+ socket.connect(new InetSocketAddress(ip, port), timeout);
+ socket.close();
+ return true;
+ } catch (Exception ex) {
+ return false;
+ }
+ }
+
+ // thread utils
+ static public void setThreadStat(Context context, int threadid, int stat) {
+ Log.d(TAG, "setThreadStat:"+threadid+"-"+stat);
+ NStorage.setIntSP(context, "thread_stat_"+threadid,stat);
+ }
+
+ static public boolean isThreadsStop(Context context) {
+ boolean st = true;
+ for (int i = 1; i<= BASE_CONF.THREA_STAT.length; i++) {
+ int j = NStorage.getIntSP(context, "thread_stat_"+i);
+ Log.d(TAG, "isThreadsStop i:"+i+"-j:"+j);
+ if (j == 1) {
+ st = false;
+ }
+ }
+ Log.d(TAG, "isThreadsStop:"+st);
+ return st;
+ }
+
+ static public void clearThreadsStat(Context context) {
+ for (int i = 1; i<= BASE_CONF.THREA_STAT.length; i++) {
+ NStorage.setIntSP(context, "thread_stat_"+i,0);
+ }
+ }
+
+ public static boolean isRootEnable(Context context) {
+ boolean enabledRoot = NStorage.getSP(context, "app.root").equals("1");
+ return isRootSystem() && enabledRoot;
+ }
+
+ public static boolean isRootSystem() {
+ if(systemRootState==kSystemRootStateEnable) {
+ return true;
+ } else if(systemRootState==kSystemRootStateDisable) {
+
+ return false;
+ }
+ File f=null;
+ final String kSuSearchPaths[]={"/su/bin/", "/system/bin/","/system/xbin/","/system/sbin/","/sbin/","/vendor/bin/"};
+ try {
+ for(int i=0;i b || a < 0,返回-1
+ public static int getRandomInt(int min, int max) {
+ if (min > max || min < 0)
+ return -1;
+ // 下面两种形式等价
+ // return a + (int) (new Random().nextDouble() * (b - a + 1));
+ Random random = new Random();
+ int s = random.nextInt(max)%(max-min+1) + min;
+ return s;
+ }
+ public static Map> getQueryParams(String url) {
+ try {
+ Map> params = new HashMap>();
+ String[] urlParts = url.split("\\?");
+ if (urlParts.length > 1) {
+ String query = urlParts[1];
+ for (String param : query.split("&")) {
+ String[] pair = param.split("=");
+ String key = URLDecoder.decode(pair[0], "UTF-8");
+ String value = "";
+ if (pair.length > 1) {
+ value = URLDecoder.decode(pair[1], "UTF-8");
+ }
+
+ List values = params.get(key);
+ if (values == null) {
+ values = new ArrayList();
+ params.put(key, values);
+ }
+ values.add(value);
+ }
+ }
+
+ return params;
+ } catch (UnsupportedEncodingException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+ /**
+ * Convert byte array to hex string
+ * @param bytes toConvert
+ * @return hexValue
+ */
+ public static String bytesToHex(byte[] bytes) {
+ StringBuilder sbuf = new StringBuilder();
+ for(int idx=0; idx < bytes.length; idx++) {
+ int intVal = bytes[idx] & 0xff;
+ if (intVal < 0x10) sbuf.append("0");
+ sbuf.append(Integer.toHexString(intVal).toUpperCase());
+ }
+ return sbuf.toString();
+ }
+
+ /**
+ * Get utf8 byte array.
+ * @param str which to be converted
+ * @return array of NULL if error was found
+ */
+ public static byte[] getUTF8Bytes(String str) {
+ try { return str.getBytes("UTF-8"); } catch (Exception ex) { return null; }
+ }
+
+ /**
+ * Load UTF8withBOM or any ansi text file.
+ * @param filename which to be converted to string
+ * @return String value of File
+ * @throws java.io.IOException if error occurs
+ */
+ public static String loadFileAsString(String filename) throws java.io.IOException {
+ final int BUFLEN=1024;
+ BufferedInputStream is = new BufferedInputStream(new FileInputStream(filename), BUFLEN);
+ try {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFLEN);
+ byte[] bytes = new byte[BUFLEN];
+ boolean isUTF8=false;
+ int read,count=0;
+ while((read=is.read(bytes)) != -1) {
+ if (count==0 && bytes[0]==(byte)0xEF && bytes[1]==(byte)0xBB && bytes[2]==(byte)0xBF ) {
+ isUTF8=true;
+ baos.write(bytes, 3, read-3); // drop UTF8 bom marker
+ } else {
+ baos.write(bytes, 0, read);
+ }
+ count+=read;
+ }
+ return isUTF8 ? new String(baos.toByteArray(), "UTF-8") : new String(baos.toByteArray());
+ } finally {
+ try{ is.close(); } catch(Exception ignored){}
+ }
+ }
+
+ /**
+ * Returns MAC address of the given interface name.
+ * @param interfaceName eth0, wlan0 or NULL=use first interface
+ * @return mac address or empty string
+ */
+ public static String getMACAddress(String interfaceName) {
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ if (interfaceName != null) {
+ if (!intf.getName().equalsIgnoreCase(interfaceName)) continue;
+ }
+ byte[] mac = intf.getHardwareAddress();
+ if (mac==null) return "";
+ StringBuilder buf = new StringBuilder();
+ for (byte aMac : mac) buf.append(String.format("%02X:",aMac));
+ if (buf.length()>0) buf.deleteCharAt(buf.length()-1);
+ return buf.toString();
+ }
+ } catch (Exception ignored) { } // for now eat exceptions
+ return "";
+ /*try {
+ // this is so Linux hack
+ return loadFileAsString("/sys/class/net/" +interfaceName + "/address").toUpperCase().trim();
+ } catch (IOException ex) {
+ return null;
+ }*/
+ }
+
+ /**
+ * Get IP address from first non-localhost interface
+ * @param useIPv4 true=return ipv4, false=return ipv6
+ * @return address or empty string
+ */
+ public static String getIPAddress(boolean useIPv4) {
+ try {
+ List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
+ for (NetworkInterface intf : interfaces) {
+ List addrs = Collections.list(intf.getInetAddresses());
+ for (InetAddress addr : addrs) {
+ if (!addr.isLoopbackAddress()) {
+ String sAddr = addr.getHostAddress();
+ //boolean isIPv4 = InetAddressUtils.isIPv4Address(sAddr);
+ boolean isIPv4 = sAddr.indexOf(':')<0;
+
+ if (useIPv4) {
+ if (isIPv4)
+ return sAddr;
+ } else {
+ if (!isIPv4) {
+ int delim = sAddr.indexOf('%'); // drop ip6 zone suffix
+ return delim<0 ? sAddr.toUpperCase() : sAddr.substring(0, delim).toUpperCase();
+ }
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) { } // for now eat exceptions
+ return "";
+ }
+
+ @SuppressLint("NewApi")
+ public static boolean checkCameraHardware(Context context) {
+ // PackageManager.FEATURE_CAMERA / PackageManager.FEATURE_CAMERA_FRONT / PackageManager.FEATURE_CAMERA_ANY
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){
+ // this device has a camera
+ return true;
+ } else {
+ // no camera on this device
+ return false;
+ }
+ }
+ public static String getCsFromRE(String pt, String content) {
+ Pattern pa1 = Pattern.compile(pt, Pattern.CASE_INSENSITIVE);
+ Matcher matcher1 = pa1.matcher(content);
+ if (matcher1.find()) {
+ return matcher1.group(1);
+ } else {
+ return "";
+ }
+ }
+ public static String getCpuProcessFamilyInfo() {
+ String cInfo = NUtil.getCpuInfo();
+ Log.d(TAG, "getCpuProcessFamilyInfo:"+cInfo);
+ String[] items = cInfo.split("\n");
+ String ret = "";
+ for (int i=0;i1) {
+ return xx[1];
+ }
+ }
+ return "";
+ }
+
+ } catch (MalformedURLException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return "";
+ }
+ }
+
+ public static String getFileFromUrl(String url) {
+ String path = getPathFromUrl(url);
+ String xx[] = path.split("/");
+ return xx[xx.length-1];
+ }
+
+ public static String getPathFromUrl(String url) {
+ URL iurl;
+ try {
+ iurl = new URL(url);
+ try {
+ return java.net.URLDecoder.decode(iurl.getPath(), "UTF-8");
+ } catch (UnsupportedEncodingException e1) {
+ return iurl.getPath();
+
+ }
+
+ } catch (MalformedURLException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return "unkown.dat";
+ }
+ }
+
+ @TargetApi(4)
+ public static String getCpuType() {
+ return Build.CPU_ABI;
+ }
+
+ public static boolean isIP(String checkStr) {
+ try {
+ String number = checkStr.substring(0,checkStr.indexOf('.'));
+ if(Integer.parseInt(number) > 255)
+ return false;
+ checkStr = checkStr.substring(checkStr.indexOf('.')+ 1);
+ number = checkStr.substring(0,checkStr.indexOf('.'));
+ if(Integer.parseInt(number) > 255)
+ return false;
+ checkStr = checkStr.substring(checkStr.indexOf('.')+ 1);
+ number = checkStr.substring(0,checkStr.indexOf('.'));
+ if(Integer.parseInt(number) > 255)
+ return false;
+ number = checkStr.substring(checkStr.indexOf('.')+ 1);
+ if (Integer.parseInt(number) > 255)
+ return false;
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static boolean isInt(String str) {
+ try {
+ Integer.parseInt(str) ;
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ public static String getLang() {
+ return Locale.getDefault().getLanguage();
+ }
+
+ private static final String TAG = "NUtil";
+
+ public static String sescape(String str) {
+ try {
+ return str.replace("'", "_").replace("\"","_").replace(":", "_").replace("+", "_").replace("?", "_").replace("!", "_").replace("#", "_").replace("(", "_").replace(")", "_").replace("{", "_").replace("}", "_").replace("\\", "_").replace("&", "_").replace("\n","_").replace("|", "_").replace("*", "_").replace("/", "_").replace(",", "_");
+ } catch (NullPointerException e) {
+ return "unkown";
+ }
+ }
+ public static String getLocalNumber(Context context) {
+ try {
+ TelephonyManager tManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ @SuppressLint("MissingPermission") String number = tManager.getLine1Number();
+ return number;
+ } catch (Exception e) {
+ return "x";
+ }
+ }
+
+ public static String getIMEI(Context context) {
+ try {
+ TelephonyManager telephonyManager=(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ @SuppressLint("MissingPermission") String imei=telephonyManager.getDeviceId();
+ return imei;
+ } catch (Exception e) {
+ return "x";
+ }
+ }
+
+ public static String getWifiMac(Context context) {
+ try {
+ WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ @SuppressLint("MissingPermission") WifiInfo info = wifi.getConnectionInfo();
+ return info.getMacAddress();
+ } catch (Exception e) {
+ return "x";
+ }
+ }
+
+ public static int getRate(long c, long t) {
+ double k = (double) (100*c/t);
+ int x = (int)k;
+
+ //Log.d(TAG, "getRate:"+ x);
+ return x;
+ }
+ public static String getSizeAsKS(long size) {
+ //java.text.DecimalFormat df = new java.text.DecimalFormat("#.##");
+ double s = (double) ((size/1024));
+ //return df.format(s)+"";
+ int x = (int)s;
+ if (x==0) {
+ return "< 1";
+ } else {
+ return x+"";
+ }
+ }
+ public static String getSizeAsMS(long size) {
+ //java.text.DecimalFormat df = new java.text.DecimalFormat("#.##");
+ double s = (double) ((size/1024)/1024);
+ //return df.format(s)+"";
+ int x = (int)s;
+ if (x==0) {
+ return "< 1";
+ } else {
+ return x+"";
+ }
+ }
+ public static String getSizeAsK(long size) {
+ //java.text.DecimalFormat df = new java.text.DecimalFormat("#.##");
+ double s = (double) ((size/1024));
+ //return df.format(s)+"";
+ int x = (int)s;
+ if (x==0) {
+ return "< 1";
+ } else {
+ return x+"";
+ }
+ }
+ public static int getSizeAsM(long size) {
+ //java.text.DecimalFormat df = new java.text.DecimalFormat("#.##");
+ double s = (double) ((size/1024)/1024);
+ //return df.format(s)+"";
+ int x = (int)s;
+ return x;
+ }
+
+ public static List getAllApps(Context context) {
+ List apps = new ArrayList();
+ PackageManager pManager = context.getPackageManager();
+ //获取手机内所有应用
+ List paklist = pManager.getInstalledPackages(0);
+ for (int i = 0; i < paklist.size(); i++) {
+ PackageInfo pak = (PackageInfo) paklist.get(i);
+ //判断是否为非系统预装的应用程序
+ //if ((pak.applicationInfo.flags & pak.applicationInfo.FLAG_SYSTEM) <= 0) {
+ // customs applications
+ apps.add(pak);
+ //}
+ }
+ return apps;
+ }
+ public static boolean checkAppInstalledByName(Context context, Intent intent) {
+ PackageManager pm = context.getPackageManager();
+ List activities = pm.queryIntentActivities(intent, 0);
+ if (activities.size() == 0) {
+ if (true) Log.d(TAG, "packaged not installed:"+intent.getAction());
+
+ return false;
+ } else {
+ if (true) Log.d(TAG, "packaged installed:"+intent.getAction());
+
+ return true;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static boolean checkAppInstalledByName(Context context, String packageName) {
+ if (packageName == null || "".equals(packageName))
+ return false;
+ try {
+ ApplicationInfo info = context.getPackageManager().getApplicationInfo(
+ packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
+
+ //Log.d(TAG, "checkAppInstalledByName:"+packageName+" found");
+ return true;
+ } catch (NameNotFoundException e) {
+ //Log.d(TAG, "checkAppInstalledByName:"+packageName+" not found");
+
+ return false;
+ }
+
+
+ /*List apps = NUtil.getAllApps(context);
+ for(int i=0;i acts = getPackageManager().queryIntentActivities(
+ intent, 0);
+ if (acts.size() > 0) {
+ startActivity(intent);
+ } else {
+ Toast.makeText(this,
+ getString(R.string.failed_to_resolve_activity),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ */
+ }
+
+ public static double formatd6e(double m) {
+ return (double)((int) (m * 1E6)/1E6);
+ }
+
+ public static double round(double value, int scale, int roundingMode) {
+ BigDecimal bd = new BigDecimal(value);
+ bd = bd.setScale(scale, roundingMode);
+ double d = bd.doubleValue();
+ bd = null;
+ return d;
+ }
+
+ public static void setListViewHeightBasedOnChildren(ListView listView) {
+ ListAdapter listAdapter = listView.getAdapter();
+ if (listAdapter == null) {
+ // pre-condition
+ return;
+ }
+
+ int totalHeight = 0;
+ for (int i = 0; i < listAdapter.getCount(); i++) {
+ View listItem = listAdapter.getView(i, null, listView);
+ listItem.measure(0, 0);
+ totalHeight += listItem.getMeasuredHeight();
+ }
+
+ ViewGroup.LayoutParams params = listView.getLayoutParams();
+ params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
+ listView.setLayoutParams(params);
+ }
+
+ public static int getSCWidth(Context context) {
+ WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ int width = wm.getDefaultDisplay().getWidth();//屏幕宽度
+ //Toast.makeText(context, "Screen width:"+width, Toast.LENGTH_SHORT).show();
+ return width;
+ }
+
+ public static void myNotify(Context context, String info) {
+ Toast.makeText(context, info, Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * 获取应用程序版本编号
+ * @param context
+ * @return
+ */
+ public static int getVersionCode(Context context){
+ int intVersioinCode=0;
+ try {
+ PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ intVersioinCode=info.versionCode;
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return intVersioinCode;
+ }
+
+
+ /**
+ * 获取应用程序版本号
+ * @param context
+ * @return
+ */
+ public static String getVersionName(Context context){
+ String strVersionName=null;
+ try {
+ PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+ strVersionName=info.versionName;
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ return strVersionName;
+ }
+
+ public static ProgressDialog progressWindow(Context context, int resourceId) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ if (resourceId!=0) {
+ dialog.setMessage(context.getString(resourceId));
+ }
+ return dialog;
+ /*
+ LayoutInflater li = LayoutInflater.from(context);
+ View view = li.inflate(R.layout.m_waiting, null);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ //builder.setTitle(context.getString(resourceId));
+ //builder.setIcon(R.drawable.icon);
+ //之前inflate的View 放到dialog中
+ builder.setView(view);
+
+ builder.create();
+ return builder;
+ */
+ }
+
+ @SuppressLint("NewApi")
+ public static ProgressDialog progressWindow(Context context, String message) {
+ ProgressDialog dialog = new ProgressDialog(context);
+ if (!message.isEmpty()) {
+ dialog.setMessage(message);
+ }
+ return dialog;
+ /*
+ LayoutInflater li = LayoutInflater.from(context);
+ View view = li.inflate(R.layout.m_waiting, null);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ //builder.setTitle(context.getString(resourceId));
+ //builder.setIcon(R.drawable.icon);
+ //之前inflate的View 放到dialog中
+ builder.setView(view);
+
+ builder.create();
+ return builder;
+ */
+ }
+
+ public static boolean in_array(String[] haystack, String needle) {
+ for(int i=0;i services = activityManager.getRunningServices(Integer.MAX_VALUE);
+
+ for (RunningServiceInfo runningServiceInfo : services) {
+ if (runningServiceInfo.service.getClassName().equals(serviceName)){
+ return true;
+ }
+ }
+ return false;
+
+/* ActivityManager myAM=(ActivityManager)c.getSystemService(Context.ACTIVITY_SERVICE);
+
+ ArrayList runningServices = (ArrayList) myAM.getRunningServices(60);
+ //获取最多60个当前正在运行的服务,放进ArrList里,以现在手机的处理能力,要是超过40个服务,估计已经卡死,所以不用考虑超过40个该怎么办
+ for(int i = 0 ; i setRandomList(List strs){
+ SIZE=strs.size();
+ changePositions(strs);
+ return strs;
+ }
+
+ public static JSONArray setRandomArray(JSONArray objs){
+ SIZE=objs.length();
+ changePositions(objs);
+ return objs;
+ }
+
+ public static void changePositions(JSONArray objs){
+ for(int i=SIZE-1;i>0;i--){
+ exchange(objs,random.nextInt(i+1),i);
+ }
+ }
+
+
+ public static void changePositions(List strs){
+ for(int i=SIZE-1;i>0;i--){
+ exchange(strs,random.nextInt(i+1),i);
+ }
+ }
+
+ private static void exchange(JSONArray objs,int p1,int p2){
+ JSONObject temp;
+ try {
+ temp = (JSONObject)objs.get(p1);
+ JSONObject op2 = (JSONObject)objs.get(p2);
+ objs.put(p1, op2);
+ objs.put(p2, temp);
+ }
+ catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+
+
+ private static void exchange(List strs,int p1,int p2){
+ String temp=strs.get(p1);
+ strs.set(p1, strs.get(p2));
+ strs.set(p2, temp);
+ }
+
+
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/StreamGobbler.java b/qbaselib/src/main/java/com/quseit/util/StreamGobbler.java
new file mode 100644
index 00000000..d5fda6fc
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/StreamGobbler.java
@@ -0,0 +1,308 @@
+package com.quseit.util;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A StreamGobbler is an InputStream that uses an internal worker thread to constantly consume input from
+ * another InputStream. It uses a buffer to store the consumed data. The buffer size is automatically adjusted, if
+ * needed.
+ *
+ * This class is sometimes very convenient - if you wrap a session's STDOUT and STDERR InputStreams with instances of
+ * this class, then you don't have to bother about the shared window of STDOUT and STDERR in the low level SSH-2
+ * protocol, since all arriving data will be immediatelly consumed by the worker threads. Also, as a side effect, the
+ * streams will be buffered (e.g., single byte read() operations are faster).
+ *
+ * Other SSH for Java libraries include this functionality by default in their STDOUT and STDERR InputStream
+ * implementations, however, please be aware that this approach has also a downside:
+ *
+ * If you do not call the StreamGobbler's read() method often enough and the peer is constantly sending
+ * huge amounts of data, then you will sooner or later encounter a low memory situation due to the aggregated data
+ * (well, it also depends on the Java heap size). Joe Average will like this class anyway - a paranoid programmer would
+ * never use such an approach.
+ *
+ * The term "StreamGobbler" was taken from an article called "When Runtime.exec() won't", see
+ * http://www.javaworld.com/javaworld/jw-12-2000/jw-1229-traps.html.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: StreamGobbler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+
+public class StreamGobbler extends InputStream {
+ class GobblerThread extends Thread {
+ /*
+ @Override
+ public void run() {
+
+ while (true) {
+ try {
+ byte[] saveBuffer = null;
+
+ int avail = is.read(buffer, write_pos, buffer.length - write_pos);
+ synchronized (synchronizer) {
+ if (avail <= 0) {
+ isEOF = true;
+ synchronizer.notifyAll();
+ break;
+ }
+ write_pos += avail;
+
+ int space_available = buffer.length - write_pos;
+ Log.e("space_available:" + buffer.length + "-" + write_pos);
+
+ if (space_available == 0) {
+ if (read_pos > 0) {
+ saveBuffer = new byte[read_pos];
+ System.arraycopy(buffer, 0, saveBuffer, 0, read_pos);
+ System.arraycopy(buffer, read_pos, buffer, 0, buffer.length - read_pos);
+ write_pos -= read_pos;
+ read_pos = 0;
+
+ Log.e("read_pos > 0:" + buffer);
+
+ }
+ else {
+ write_pos = 0;
+ saveBuffer = buffer;
+
+ Log.e("read_pos <=0 :" + buffer);
+
+ }
+ }
+
+ synchronizer.notifyAll();
+ }
+ writeToFile(saveBuffer);
+
+ }
+ catch (IOException e) {
+ synchronized (synchronizer) {
+ exception = e;
+ synchronizer.notifyAll();
+ break;
+ }
+ }
+ }
+ }*/
+
+ public void run() {
+ byte[] buff = new byte[8192];
+
+ while (true) {
+ try {
+ int avail = is.read(buff);
+
+ synchronized (synchronizer) {
+ if (avail <= 0) {
+ isEOF = true;
+ synchronizer.notifyAll();
+ break;
+ }
+
+ int space_available = buffer.length - write_pos;
+
+ if (space_available < avail) {
+
+ int unread_size = write_pos - read_pos;
+ int need_space = unread_size + avail;
+
+ byte[] new_buffer = buffer;
+
+ if (need_space > buffer.length) {
+ int inc = need_space / 3;
+ inc = (inc < 256) ? 256 : inc;
+ inc = (inc > 8192) ? 8192 : inc;
+ new_buffer = new byte[need_space + inc];
+ }
+
+ if (unread_size > 0)
+ System.arraycopy(buffer, read_pos, new_buffer, 0, unread_size);
+
+ buffer = new_buffer;
+
+ read_pos = 0;
+ write_pos = unread_size;
+ }
+
+ byte[] s_buffer = new byte[avail];
+ System.arraycopy(buff, 0, s_buffer, 0, avail);
+ System.arraycopy(buff, 0, buffer, write_pos, avail);
+ write_pos += avail;
+
+ synchronizer.notifyAll();
+ writeToFile(s_buffer);
+ // Log.e("OUTPUT:"+String.valueOf(s_buffer));
+ }
+
+ }
+ catch (IOException e) {
+ synchronized (synchronizer) {
+ exception = e;
+ synchronizer.notifyAll();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private InputStream is;
+
+ private GobblerThread t;
+
+ private Object synchronizer = new Object();
+
+ private boolean isEOF = false;
+
+ private boolean isClosed = false;
+
+ private IOException exception = null;
+
+ private byte[] buffer;
+
+ private int read_pos = 0;
+
+ private int write_pos = 0;
+
+ private final FileOutputStream mLogStream;
+
+ private final int mBufferSize;
+
+ public StreamGobbler(InputStream is, File log, int buffer_size) {
+ this.is = is;
+ mBufferSize = buffer_size;
+ FileOutputStream out = null;
+ try {
+ out = new FileOutputStream(log, false);
+ }
+ catch (IOException e) {
+ Log.e("StreamGobbler", e.getLocalizedMessage());
+ }
+ mLogStream = out;
+ buffer = new byte[mBufferSize];
+ t = new GobblerThread();
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public void writeToFile(byte[] buffer) {
+
+ if (mLogStream != null && buffer != null) {
+ try {
+ mLogStream.write(buffer);
+ }
+ catch (IOException e) {
+ Log.e("StreamGobbler",e.getLocalizedMessage());
+ }
+ }
+ }
+
+ @Override
+ public int read() throws IOException {
+ synchronized (synchronizer) {
+ if (isClosed) {
+ throw new IOException("This StreamGobbler is closed.");
+ }
+
+ while (read_pos == write_pos) {
+ if (exception != null) {
+ throw exception;
+ }
+
+ if (isEOF) {
+ return -1;
+ }
+
+ try {
+ synchronizer.wait();
+ }
+ catch (InterruptedException e) {
+ }
+ }
+
+ int b = buffer[read_pos++] & 0xff;
+
+ return b;
+ }
+ }
+
+ @Override
+ public int available() throws IOException {
+ synchronized (synchronizer) {
+ if (isClosed) {
+ throw new IOException("This StreamGobbler is closed.");
+ }
+
+ return write_pos - read_pos;
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public void close() throws IOException {
+ synchronized (synchronizer) {
+ if (isClosed) {
+ return;
+ }
+ isClosed = true;
+ isEOF = true;
+ synchronizer.notifyAll();
+ is.close();
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (b == null) {
+ throw new NullPointerException();
+ }
+
+ if ((off < 0) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0) || (off > b.length)) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ if (len == 0) {
+ return 0;
+ }
+
+ synchronized (synchronizer) {
+ if (isClosed) {
+ throw new IOException("This StreamGobbler is closed.");
+ }
+
+ while (read_pos == write_pos) {
+ if (exception != null) {
+ throw exception;
+ }
+
+ if (isEOF) {
+ return -1;
+ }
+
+ try {
+ synchronizer.wait();
+ }
+ catch (InterruptedException e) {
+ }
+ }
+
+ int avail = write_pos - read_pos;
+
+ avail = (avail > len) ? len : avail;
+
+ System.arraycopy(buffer, read_pos, b, off, avail);
+
+ read_pos += avail;
+
+ return avail;
+ }
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/StringUtils.java b/qbaselib/src/main/java/com/quseit/util/StringUtils.java
new file mode 100644
index 00000000..e45df574
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/StringUtils.java
@@ -0,0 +1,90 @@
+package com.quseit.util;
+
+import android.annotation.SuppressLint;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+
+public class StringUtils {
+ public static String addSlashes(String txt)
+ {
+ if (null != txt)
+ {
+ txt = txt.replace("\\", "\\\\") ;
+ txt = txt.replace("'", "\\'") ;
+ //txt = txt.replace(" ", "\\ ") ;
+
+ }
+
+ return txt ;
+ }
+
+ public static String join(Collection collection, String delimiter) {
+ StringBuffer buffer = new StringBuffer();
+ Iterator iter = collection.iterator();
+ while (iter.hasNext()) {
+ buffer.append(iter.next());
+ if (iter.hasNext()) {
+ buffer.append(delimiter);
+ }
+ }
+ return buffer.toString();
+ }
+
+ //乘着船 添加
+ public static void argvParse(String argString, ArrayList argArray){
+ if (argString == null) return;
+ argString = argString.trim();
+ int l = argString.length();
+ if(l == 0) return;
+ int i = 0,//参数起始点
+ j = 0;//参数终止点
+ char c,d;
+ StringBuilder sb = new StringBuilder();
+ while(j= l)
+ continue;
+ d = argString.charAt(j);
+ while (d != c) {
+ if (d == '\\' && c == '"' && j < l - 1) {
+ sb.append(argString, i, j);
+ j++;
+ sb.append(argString, j, j + 1);
+ j++;
+ i = j;
+ } else j++;
+ d = argString.charAt(j);
+ }
+ sb.append(argString, i, j);
+ j++;
+ i = j;
+ } else {
+ j++;
+ }
+ }
+ sb.append(argString,i,l);
+ argArray.add(sb.toString());
+ }
+
+ @SuppressLint("SimpleDateFormat")
+ public static String getDateStr(){
+ return new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/UnhandledExceptionHandler.java b/qbaselib/src/main/java/com/quseit/util/UnhandledExceptionHandler.java
new file mode 100644
index 00000000..e190ffd0
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/UnhandledExceptionHandler.java
@@ -0,0 +1,110 @@
+package com.quseit.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+
+public class UnhandledExceptionHandler implements UncaughtExceptionHandler {
+
+ private UncaughtExceptionHandler defaultUEH;
+
+ /*
+ * if any of the parameters is null, the respective functionality
+ * will not be used
+ */
+ public UnhandledExceptionHandler() {
+ this("/sdcard/");
+ }
+
+ public UnhandledExceptionHandler(String localPath) {
+ this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
+ }
+
+ public void uncaughtException(Thread t, Throwable e) {
+
+ if (!"sdk".equals(android.os.Build.MODEL) &&
+ !"google_sdk".equals(android.os.Build.MODEL))
+ {
+ StringBuilder sb = new StringBuilder();
+
+ Log.e("Exception", "Uncaught Exception", e);
+ e.printStackTrace();
+
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeInMillis(System.currentTimeMillis());
+
+ String format = "yyyy-MM-dd'T'HH:mm:ss";
+ SimpleDateFormat sdf = new SimpleDateFormat(format);
+
+ sb.append("-------------\n");
+ sb.append("Time of crash: " + sdf.format(cal.getTime()) + "\n");
+ sb.append("Phone: " + android.os.Build.MODEL + "\n");
+ sb.append("Android Version: " + android.os.Build.VERSION.RELEASE + "\n");
+ sb.append("-------------\n");
+
+ final Writer result = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(result);
+ e.printStackTrace(printWriter);
+ sb.append(result.toString());
+ printWriter.close();
+
+ //StringWriter sw = new StringWriter();
+ //StringWriter logw = new StringWriter();
+
+ //JsonWriter w = new JsonWriter(sw);
+ //String uuid = "0";
+ /*
+ if (ConfigurationManager.getInstance().getLayoutInflater() != null) {
+ uuid = android.provider.Settings.Secure.getString(
+ ConfigurationManager.getInstance().getLayoutInflater().getContext().getContentResolver(),
+ android.provider.Settings.Secure.ANDROID_ID);
+ }*/
+
+ /*try {
+ w.beginObject();
+
+ w.name("uuid");
+ w.value(uuid);
+
+ w.name("report");
+ w.value(sb.toString());
+
+ w.name("log");
+ Log.dump(logw);
+ w.value(logw.toString());
+
+ w.endObject();
+ w.flush();
+
+ //Net.getHTTPContent("http://mobile.nativeconcierge.com/api/report/crash/", "request=" + sw.toString(), "application/x-www-form-urlencoded");
+ } catch (IOException e1) {
+ }*/
+
+ //String filename = "nativeconcierge-crash-" + timestamp + ".stacktrace";
+ //writeToFile(stacktrace, filename);
+ }
+
+ defaultUEH.uncaughtException(t, e);
+ }
+
+ //private void writeToFile(String stacktrace, String filename) {
+ /*try {
+ File root = ConfigurationManager.getFilesDir();
+ if (root.canWrite()) {
+ File file = new File(root, filename);
+ //Log.d("Exception", "Writing to " + file.getAbsolutePath());
+ BufferedWriter bos = new BufferedWriter(new FileWriter(file));
+ bos.write(stacktrace);
+ bos.flush();
+ bos.close();
+ } else {
+ //Log.d("Exception", "Can't write to " + root.getAbsolutePath());
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }*/
+ //}
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/Utils.java b/qbaselib/src/main/java/com/quseit/util/Utils.java
new file mode 100644
index 00000000..d9a85e13
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/Utils.java
@@ -0,0 +1,412 @@
+package com.quseit.util;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ConfigurationInfo;
+import android.graphics.Bitmap;
+import android.os.Environment;
+import android.util.Log;
+import android.widget.Toast;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import util.FileUtil;
+
+public class Utils {
+ private static final String TAG = "Utils";
+
+ public static List copyIterator(Iterator iter) {
+ List copy = new ArrayList();
+ while (iter.hasNext())
+ copy.add(iter.next());
+ return copy;
+ }
+
+ public static boolean ifPyRunOk(String file) {
+ /*String content = FileHelper.getFileContents(file);
+ if (content.contains("Traceback")
+ || content.contains("Error:")) {
+ return false;
+ } else {
+ return true;
+ }*/
+ return true;
+
+ }
+ public static String getFileExtension(String sFileName) {
+ int dotIndex = sFileName.lastIndexOf('.');
+ if (dotIndex == -1) {
+ return null;
+ }
+ return sFileName.substring(dotIndex);
+ }
+
+ //-------------------------------------------------------------------------------------------------
+
+ public static boolean unzip(InputStream inputStream, String dest, boolean replaceIfExists) {
+ Log.d(TAG, "unzip:"+dest);
+ final int BUFFER_SIZE = 4096;
+
+ BufferedOutputStream bufferedOutputStream = null;
+
+ boolean succeed = true;
+
+ if (replaceIfExists) {
+ File file2 = new File(dest);
+ if (file2.exists()) {
+ try {
+ //boolean b = deleteDir(file2);
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ try {
+ ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(inputStream));
+ ZipEntry zipEntry;
+
+ while ((zipEntry = zipInputStream.getNextEntry()) != null){
+
+ String zipEntryName = zipEntry.getName();
+ String fs = dest + zipEntryName;
+
+ if (!dest.endsWith("/")) {
+ fs = dest;
+ }
+ //Log.d(TAG, "zipEntryName:"+zipEntryName+"-file2:"+fs+"-"+fs.indexOf('/'));
+
+// if(!zipEntry.isDirectory()) {
+// File fil = new File(dest + zipEntryName);
+// fil.getParent()
+// }
+
+ // file exists ? delete ?
+ /*File file2 = new File(fs);
+ if(file2.exists()) {
+ if (replaceIfExists) {
+
+ try {
+ boolean b = deleteDir(file2);
+ if(!b) {
+ Log.e(TAG, "Unzip failed to delete " + dest + zipEntryName);
+ }
+ else {
+ Log.d(TAG, "Unzip deleted " + dest + zipEntryName);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Unzip failed to delete " + dest + zipEntryName, e);
+ }
+ }
+ }*/
+
+ // extract
+ File file = new File(fs);
+
+ if (!replaceIfExists && file.exists()){
+ Log.d(TAG, "unzip exists");
+ } else {
+ if(zipEntry.isDirectory()){
+ file.mkdirs();
+ FileUtil.chmod(file, 0755);
+
+ }else{
+
+ // create parent file folder if not exists yet
+ if(!file.getParentFile().exists()) {
+ file.getParentFile().mkdirs();
+ FileUtil.chmod(file.getParentFile(), 0755);
+ }
+
+ byte buffer[] = new byte[BUFFER_SIZE];
+ bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE);
+ int count;
+
+ while ((count = zipInputStream.read(buffer, 0, BUFFER_SIZE)) != -1) {
+ bufferedOutputStream.write(buffer, 0, count);
+ }
+
+ bufferedOutputStream.flush();
+ bufferedOutputStream.close();
+ }
+ }
+
+ if(file.getName().endsWith(".so")) {
+ FileUtil.chmod(file, 0755);
+ }
+
+ Log.d(TAG,"Unzip extracted " + dest + zipEntryName);
+ }
+
+
+ zipInputStream.close();
+
+ } catch (FileNotFoundException e) {
+ Log.e(TAG,"Unzip error, file not found", e);
+ succeed = false;
+ }catch (Exception e) {
+ Log.e(TAG,"Unzip error: ", e);
+ succeed = false;
+ }
+
+ return succeed;
+ }
+
+
+ //-------------------------------------------------------------------------------------------------
+
+ public static boolean deleteDir(File dir) {
+ try {
+ if (dir.isDirectory()) {
+ String[] children = dir.list();
+ for (int i=0; i cls) {
+ return cls.getName();
+ }
+
+ public static void showToast(Context context, String str) {
+ Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
+ }
+
+ /**
+ * 检查是否存在SD卡
+ *
+ * @return
+ */
+ public static boolean hasSdcard() {
+ String state = Environment.getExternalStorageState();
+ if (state.equals(Environment.MEDIA_MOUNTED)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * 创建目录
+ *
+ * @param context
+ * @param dirName
+ * 文件夹名称
+ * @return
+ */
+ public static File createFileDir(Context context, String dirName) {
+ String filePath;
+ // 如SD卡已存在,则存储;反之存在data目录下
+ if (hasSdcard()) {
+ // SD卡路径
+ filePath = Environment.getExternalStorageDirectory()
+ + File.separator + dirName;
+ } else {
+ filePath = context.getCacheDir().getPath() + File.separator
+ + dirName;
+ }
+ File destDir = new File(filePath);
+ if (!destDir.exists()) {
+ boolean isCreate = destDir.mkdirs();
+ Log.i(Util_LOG, filePath + " has created. " + isCreate);
+ }
+ return destDir;
+ }
+
+ /**
+ * 删除文件(若为目录,则递归删除子目录和文件)
+ *
+ * @param file
+ * @param delThisPath
+ * true代表删除参数指定file,false代表保留参数指定file
+ */
+ public static void delFile(File file, boolean delThisPath) {
+ if (!file.exists()) {
+ return;
+ }
+ if (file.isDirectory()) {
+ File[] subFiles = file.listFiles();
+ if (subFiles != null) {
+ int num = subFiles.length;
+ // 删除子目录和文件
+ for (int i = 0; i < num; i++) {
+ delFile(subFiles[i], true);
+ }
+ }
+ }
+ if (delThisPath) {
+ file.delete();
+ }
+ }
+
+ /**
+ * 获取文件大小,单位为byte(若为目录,则包括所有子目录和文件)
+ *
+ * @param file
+ * @return
+ */
+ public static long getFileSize(File file) {
+ long size = 0;
+ if (file.exists()) {
+ if (file.isDirectory()) {
+ File[] subFiles = file.listFiles();
+ if (subFiles != null) {
+ int num = subFiles.length;
+ for (int i = 0; i < num; i++) {
+ size += getFileSize(subFiles[i]);
+ }
+ }
+ } else {
+ size += file.length();
+ }
+ }
+ return size;
+ }
+
+ /**
+ * 保存Bitmap到指定目录
+ *
+ * @param dir
+ * 目录
+ * @param fileName
+ * 文件名
+ * @param bitmap
+ * @throws IOException
+ */
+ public static void savaBitmap(File dir, String fileName, Bitmap bitmap) {
+ Log.d("Utils", "savaBitmap:"+dir+fileName+"|"+bitmap);
+ if (bitmap == null) {
+ return;
+ }
+ File file = new File(dir, fileName);
+ try {
+ file.createNewFile();
+ FileOutputStream fos = new FileOutputStream(file);
+ bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.flush();
+ fos.close();
+ } catch (IOException e) {
+ Log.d("Utils", "savaBitmap IOException:"+e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * 判断某目录下文件是否存在
+ *
+ * @param dir
+ * 目录
+ * @param fileName
+ * 文件名
+ * @return
+ */
+ public static boolean isFileExists(File dir, String fileName) {
+ return new File(dir, fileName).exists();
+ }
+
+
+ public static String getSP(Context context, String key) {
+ String val;
+ SharedPreferences obj = context.getSharedPreferences("qpyspf",0);
+ val = obj.getString(key,"");
+ return val;
+ }
+
+ public static boolean isOpenGL2supported(Context context) {
+
+ final ActivityManager activityManager =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ final ConfigurationInfo configurationInfo =
+ activityManager.getDeviceConfigurationInfo();
+ final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
+ return supportsEs2;
+ }
+ public static boolean httpPing(String url, int timeout) {
+ //Log.d(TAG, "httpPing:"+url+"-"+timeout);
+ url = url.replaceFirst("https", "http"); // Otherwise an exception may be thrown on invalid SSL certificates.
+
+ try {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setConnectTimeout(timeout);
+ connection.setReadTimeout(timeout);
+ connection.setRequestMethod("HEAD");
+ int responseCode = connection.getResponseCode();
+ //Log.d(TAG, "responseCode:"+responseCode);
+ return (responseCode>0);
+ //return (200 <= responseCode && responseCode <= 399);
+ } catch (IOException exception) {
+ Log.d(TAG, "exception:"+exception.getLocalizedMessage());
+
+ return false;
+ }
+ }
+ static public boolean isSrvOk(String srv) {
+ try {
+ URL u = new URL(srv);
+ int port = 80;
+ if (u.getPort() != -1) {
+ port = u.getPort();
+ }
+ String url = u.getProtocol() + "://" +u.getHost()+":"+port+"/";
+ boolean ret = httpPing(url, 1000);
+ return ret;
+
+ } catch (MalformedURLException e) {
+ //Log.d("Bean", "MalformedURLException:"+e);
+ return false;
+ }
+ }
+
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/VeDate.java b/qbaselib/src/main/java/com/quseit/util/VeDate.java
new file mode 100644
index 00000000..82def42f
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/VeDate.java
@@ -0,0 +1,697 @@
+package com.quseit.util;
+
+import java.util.*;
+import java.text.*;
+import java.util.Calendar;
+
+public class VeDate {
+ /**
+ * 获取现在时间
+ *
+ * @return 返回时间类型 yyyy-MM-dd HH:mm:ss
+ */
+ public static Date getNowDate() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String dateString = formatter.format(currentTime);
+ ParsePosition pos = new ParsePosition(8);
+ Date currentTime_2 = formatter.parse(dateString, pos);
+ return currentTime_2;
+ }
+
+ /**
+ * 获取现在时间
+ *
+ * @return返回短时间格式 yyyy-MM-dd
+ */
+ public static Date getNowDateShort() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ String dateString = formatter.format(currentTime);
+ ParsePosition pos = new ParsePosition(8);
+ Date currentTime_2 = formatter.parse(dateString, pos);
+ return currentTime_2;
+ }
+
+ /**
+ * 获取现在时间
+ *
+ * @return返回字符串格式 yyyy-MM-dd HH:mm:ss
+ */
+ public static int getDateAsInt() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("ddHHmmss");
+ String dateString = formatter.format(currentTime);
+ return Integer.parseInt(dateString);
+ }
+
+ /*public static int getDatemsAsInt() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("mmss");
+ String dateString = formatter.format(currentTime);
+ return Integer.parseInt(dateString);
+ }*/
+
+ public static String getStringDate() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String dateString = formatter.format(currentTime);
+ return dateString;
+ }
+
+ public static int getStringDateHourAsInt() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMddHH");
+ String dateString = formatter.format(currentTime);
+ return Integer.parseInt(dateString);
+ }
+
+ /**
+ * 获取现在时间
+ *
+ * @return 返回短时间字符串格式yyyy-MM-dd
+ */
+ public static String getStringDateShort() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ String dateString = formatter.format(currentTime);
+ return dateString;
+ }
+
+ /**
+ * 获取时间 小时:分;秒 HH:mm:ss
+ *
+ * @return
+ */
+ public static String getTimeShort() {
+ SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
+ Date currentTime = new Date();
+ String dateString = formatter.format(currentTime);
+ return dateString;
+ }
+
+ /**
+ * 将长时间格式字符串转换为时间 yyyy-MM-dd HH:mm:ss
+ *
+ * @param strDate
+ * @return
+ */
+ public static Date strToDateLong(String strDate) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ ParsePosition pos = new ParsePosition(0);
+ Date strtodate = formatter.parse(strDate, pos);
+ return strtodate;
+ }
+
+ /**
+ * 将长时间格式时间转换为字符串 yyyy-MM-dd HH:mm:ss
+ *
+ * @param dateDate
+ * @return
+ */
+ public static String dateToStrLong(java.util.Date dateDate) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String dateString = formatter.format(dateDate);
+ return dateString;
+ }
+
+ /**
+ * 将短时间格式时间转换为字符串 yyyy-MM-dd
+ *
+ * @param dateDate
+ * @return
+ */
+ public static String dateToStr(java.util.Date dateDate) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ String dateString = formatter.format(dateDate);
+ return dateString;
+ }
+
+ /**
+ * 将短时间格式字符串转换为时间 yyyy-MM-dd
+ *
+ * @param strDate
+ * @return
+ */
+ public static Date strToDate(String strDate) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ ParsePosition pos = new ParsePosition(0);
+ Date strtodate = formatter.parse(strDate, pos);
+ return strtodate;
+ }
+
+ /**
+ * 得到现在时间
+ *
+ * @return
+ */
+ public static Date getNow() {
+ Date currentTime = new Date();
+ return currentTime;
+ }
+
+ /**
+ * 提取一个月中的最后一天
+ *
+ * @param day
+ * @return
+ */
+ public static Date getLastDate(long day) {
+ Date date = new Date();
+ long date_3_hm = date.getTime() - 3600000 * 34 * day;
+ Date date_3_hm_date = new Date(date_3_hm);
+ return date_3_hm_date;
+ }
+
+ /**
+ * 得到现在时间
+ *
+ * @return 字符串 yyyyMMdd HHmmss
+ */
+ public static String getStringToday() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmmss");
+ String dateString = formatter.format(currentTime);
+ return dateString;
+ }
+
+ /**
+ * 得到现在小时
+ */
+ public static String getHour() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String dateString = formatter.format(currentTime);
+ String hour;
+ hour = dateString.substring(11, 13);
+ return hour;
+ }
+
+ /**
+ * 得到现在分钟
+ *
+ * @return
+ */
+ public static String getTime() {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String dateString = formatter.format(currentTime);
+ String min;
+ min = dateString.substring(14, 16);
+ return min;
+ }
+
+ /**
+ * 根据用户传入的时间表示格式,返回当前时间的格式 如果是yyyyMMdd,注意字母y不能大写。
+ *
+ * @param sformat
+ * yyyyMMddhhmmss
+ * @return
+ */
+ public static String getUserDate(String sformat) {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat(sformat);
+ String dateString = formatter.format(currentTime);
+ return dateString;
+ }
+
+ /**
+ * 二个小时时间间的差值,必须保证二个时间都是"HH:MM"的格式,返回字符型的分钟
+ */
+ public static String getTwoHour(String st1, String st2) {
+ String[] kk = null;
+ String[] jj = null;
+ kk = st1.split(":");
+ jj = st2.split(":");
+ if (Integer.parseInt(kk[0]) < Integer.parseInt(jj[0]))
+ return "0";
+ else {
+ double y = Double.parseDouble(kk[0]) + Double.parseDouble(kk[1]) / 60;
+ double u = Double.parseDouble(jj[0]) + Double.parseDouble(jj[1]) / 60;
+ if ((y - u) > 0)
+ return y - u + "";
+ else
+ return "0";
+ }
+ }
+
+ /**
+ * 得到二个日期间的间隔天数
+ */
+ public static String getTwoDay(String sj1, String sj2) {
+ SimpleDateFormat myFormatter = new SimpleDateFormat("yyyy-MM-dd");
+ long day = 0;
+ try {
+ java.util.Date date = myFormatter.parse(sj1);
+ java.util.Date mydate = myFormatter.parse(sj2);
+ day = (date.getTime() - mydate.getTime()) / (24 * 60 * 60 * 1000);
+ } catch (Exception e) {
+ return "";
+ }
+ return day + "";
+ }
+
+ /**
+ * 时间前推或后推分钟,其中JJ表示分钟.
+ */
+ public static String getPreTime(String sj1, String jj) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String mydate1 = "";
+ try {
+ Date date1 = format.parse(sj1);
+ long Time = (date1.getTime() / 1000) + Integer.parseInt(jj) * 60;
+ date1.setTime(Time * 1000);
+ mydate1 = format.format(date1);
+ } catch (Exception e) {
+ }
+ return mydate1;
+ }
+
+ /**
+ * 得到一个时间延后或前移几天的时间,nowdate为时间,delay为前移或后延的天数
+ */
+ public static String getNextDay(String nowdate, String delay) {
+ try{
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ String mdate = "";
+ Date d = strToDate(nowdate);
+ long myTime = (d.getTime() / 1000) + Integer.parseInt(delay) * 24 * 60 * 60;
+ d.setTime(myTime * 1000);
+ mdate = format.format(d);
+ return mdate;
+ }catch(Exception e){
+ return "";
+ }
+ }
+
+ /**
+ * 判断是否润年
+ *
+ * @param ddate
+ * @return
+ */
+ public static boolean isLeapYear(String ddate) {
+
+ /**
+ * 详细设计: 1.被400整除是闰年,否则: 2.不能被4整除则不是闰年 3.能被4整除同时不能被100整除则是闰年
+ * 3.能被4整除同时能被100整除则不是闰年
+ */
+ Date d = strToDate(ddate);
+ GregorianCalendar gc = (GregorianCalendar) Calendar.getInstance();
+ gc.setTime(d);
+ int year = gc.get(Calendar.YEAR);
+ if ((year % 400) == 0)
+ return true;
+ else if ((year % 4) == 0) {
+ if ((year % 100) == 0)
+ return false;
+ else
+ return true;
+ } else
+ return false;
+ }
+
+ /**
+ * 返回美国时间格式 26 Apr 2006
+ *
+ * @param str
+ * @return
+ */
+ public static String getEDate(String str) {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ ParsePosition pos = new ParsePosition(0);
+ Date strtodate = formatter.parse(str, pos);
+ String j = strtodate.toString();
+ String[] k = j.split(" ");
+ return k[2] + k[1].toUpperCase() + k[5].substring(2, 4);
+ }
+
+ /**
+ * 获取一个月的最后一天
+ *
+ * @param dat
+ * @return
+ */
+ public static String getEndDateOfMonth(String dat) {// yyyy-MM-dd
+ String str = dat.substring(0, 8);
+ String month = dat.substring(5, 7);
+ int mon = Integer.parseInt(month);
+ if (mon == 1 || mon == 3 || mon == 5 || mon == 7 || mon == 8 || mon == 10 || mon == 12) {
+ str += "31";
+ } else if (mon == 4 || mon == 6 || mon == 9 || mon == 11) {
+ str += "30";
+ } else {
+ if (isLeapYear(dat)) {
+ str += "29";
+ } else {
+ str += "28";
+ }
+ }
+ return str;
+ }
+
+ /**
+ * 判断二个时间是否在同一个周
+ *
+ * @param date1
+ * @param date2
+ * @return
+ */
+ public static boolean isSameWeekDates(Date date1, Date date2) {
+ Calendar cal1 = Calendar.getInstance();
+ Calendar cal2 = Calendar.getInstance();
+ cal1.setTime(date1);
+ cal2.setTime(date2);
+ int subYear = cal1.get(Calendar.YEAR) - cal2.get(Calendar.YEAR);
+ if (0 == subYear) {
+ if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
+ return true;
+ } else if (1 == subYear && 11 == cal2.get(Calendar.MONTH)) {
+ // 如果12月的最后一周横跨来年第一周的话则最后一周即算做来年的第一周
+ if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
+ return true;
+ } else if (-1 == subYear && 11 == cal1.get(Calendar.MONTH)) {
+ if (cal1.get(Calendar.WEEK_OF_YEAR) == cal2.get(Calendar.WEEK_OF_YEAR))
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * 产生周序列,即得到当前时间所在的年度是第几周
+ *
+ * @return
+ */
+ public static String getSeqWeek() {
+ Calendar c = Calendar.getInstance(Locale.CHINA);
+ String week = Integer.toString(c.get(Calendar.WEEK_OF_YEAR));
+ if (week.length() == 1)
+ week = "0" + week;
+ String year = Integer.toString(c.get(Calendar.YEAR));
+ return year + week;
+ }
+
+ /**
+ * 获得一个日期所在的周的星期几的日期,如要找出2002年2月3日所在周的星期一是几号
+ *
+ * @param sdate
+ * @param num
+ * @return
+ */
+ public static String getWeek(String sdate, String num) {
+ // 再转换为时间
+ Date dd = VeDate.strToDate(sdate);
+ Calendar c = Calendar.getInstance();
+ c.setTime(dd);
+ if (num.equals("1")) // 返回星期一所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+ else if (num.equals("2")) // 返回星期二所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY);
+ else if (num.equals("3")) // 返回星期三所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);
+ else if (num.equals("4")) // 返回星期四所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY);
+ else if (num.equals("5")) // 返回星期五所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+ else if (num.equals("6")) // 返回星期六所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+ else if (num.equals("0")) // 返回星期日所在的日期
+ c.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
+ return new SimpleDateFormat("yyyy-MM-dd").format(c.getTime());
+ }
+
+ /**
+ * 根据一个日期,返回是星期几的字符串
+ *
+ * @param sdate
+ * @return
+ */
+ public static String getWeek(String sdate) {
+ // 再转换为时间
+ Date date = VeDate.strToDate(sdate);
+ Calendar c = Calendar.getInstance();
+ c.setTime(date);
+ // int hour=c.get(Calendar.DAY_OF_WEEK);
+ // hour中存的就是星期几了,其范围 1~7
+ // 1=星期日 7=星期六,其他类推
+ return new SimpleDateFormat("EEEE").format(c.getTime());
+ }
+ public static String getWeekStr(String sdate){
+ String str = "";
+ str = VeDate.getWeek(sdate);
+ if("1".equals(str)){
+ str = "星期日";
+ }else if("2".equals(str)){
+ str = "星期一";
+ }else if("3".equals(str)){
+ str = "星期二";
+ }else if("4".equals(str)){
+ str = "星期三";
+ }else if("5".equals(str)){
+ str = "星期四";
+ }else if("6".equals(str)){
+ str = "星期五";
+ }else if("7".equals(str)){
+ str = "星期六";
+ }
+ return str;
+ }
+
+ /**
+ * 两个时间之间的天数
+ *
+ * @param date1
+ * @param date2
+ * @return
+ */
+ public static long getDays(String date1, String date2) {
+ if (date1 == null || date1.equals(""))
+ return 0;
+ if (date2 == null || date2.equals(""))
+ return 0;
+ // 转换为标准时间
+ SimpleDateFormat myFormatter = new SimpleDateFormat("yyyy-MM-dd");
+ java.util.Date date = null;
+ java.util.Date mydate = null;
+ try {
+ date = myFormatter.parse(date1);
+ mydate = myFormatter.parse(date2);
+ } catch (Exception e) {
+ }
+ long day = (date.getTime() - mydate.getTime()) / (24 * 60 * 60 * 1000);
+ return day;
+ }
+
+ /**
+ * 形成如下的日历 , 根据传入的一个时间返回一个结构 星期日 星期一 星期二 星期三 星期四 星期五 星期六 下面是当月的各个时间
+ * 此函数返回该日历第一行星期日所在的日期
+ *
+ * @param sdate
+ * @return
+ */
+ public static String getNowMonth(String sdate) {
+ // 取该时间所在月的一号
+ sdate = sdate.substring(0, 8) + "01";
+
+ // 得到这个月的1号是星期几
+ Date date = VeDate.strToDate(sdate);
+ Calendar c = Calendar.getInstance();
+ c.setTime(date);
+ int u = c.get(Calendar.DAY_OF_WEEK);
+ String newday = VeDate.getNextDay(sdate, (1 - u) + "");
+ return newday;
+ }
+
+ /**
+ * 取得数据库主键 生成格式为yyyymmddhhmmss+k位随机数
+ *
+ * @param k
+ * 表示是取几位随机数,可以自己定
+ */
+
+ public static String getNo(int k) {
+
+ return getUserDate("yyyyMMddhhmmss") + getRandom(k);
+ }
+
+ /**
+ * 返回一个随机数
+ *
+ * @param i
+ * @return
+ */
+ public static String getRandom(int i) {
+ Random jjj = new Random();
+ // int suiJiShu = jjj.nextInt(9);
+ if (i == 0)
+ return "";
+ String jj = "";
+ for (int k = 0; k < i; k++) {
+ jj = jj + jjj.nextInt(9);
+ }
+ return jj;
+ }
+
+ /**
+ *
+ * @param date
+ * @return boolean
+ */
+ public static boolean RightDate(String date) {
+
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
+ ;
+ if (date == null)
+ return false;
+ if (date.length() > 10) {
+ sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
+ } else {
+ sdf = new SimpleDateFormat("yyyy-MM-dd");
+ }
+ try {
+ sdf.parse(date);
+ } catch (ParseException pe) {
+ return false;
+ }
+ return true;
+ }
+
+ /***************************************************************************
+ * //nd=1表示返回的值中包含年度 //yf=1表示返回的值中包含月份 //rq=1表示返回的值中包含日期 //format表示返回的格式 1
+ * 以年月日中文返回 2 以横线-返回 // 3 以斜线/返回 4 以缩写不带其它符号形式返回 // 5 以点号.返回
+ **************************************************************************/
+ public static String getStringDateMonth(String sdate, String nd, String yf, String rq, String format) {
+ Date currentTime = new Date();
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ String dateString = formatter.format(currentTime);
+ String s_nd = dateString.substring(0, 4); // 年份
+ String s_yf = dateString.substring(5, 7); // 月份
+ String s_rq = dateString.substring(8, 10); // 日期
+ String sreturn = "";
+ if (sdate == null || sdate.equals("")) { // 处理空值情况
+ if (nd.equals("1")) {
+ sreturn = s_nd;
+ // 处理间隔符
+ if (format.equals("1"))
+ sreturn = sreturn + "年";
+ else if (format.equals("2"))
+ sreturn = sreturn + "-";
+ else if (format.equals("3"))
+ sreturn = sreturn + "/";
+ else if (format.equals("5"))
+ sreturn = sreturn + ".";
+ }
+ // 处理月份
+ if (yf.equals("1")) {
+ sreturn = sreturn + s_yf;
+ if (format.equals("1"))
+ sreturn = sreturn + "月";
+ else if (format.equals("2"))
+ sreturn = sreturn + "-";
+ else if (format.equals("3"))
+ sreturn = sreturn + "/";
+ else if (format.equals("5"))
+ sreturn = sreturn + ".";
+ }
+ // 处理日期
+ if (rq.equals("1")) {
+ sreturn = sreturn + s_rq;
+ if (format.equals("1"))
+ sreturn = sreturn + "日";
+ }
+ } else {
+ // 不是空值,也是一个合法的日期值,则先将其转换为标准的时间格式
+ sdate = VeDate.getOKDate(sdate);
+ s_nd = sdate.substring(0, 4); // 年份
+ s_yf = sdate.substring(5, 7); // 月份
+ s_rq = sdate.substring(8, 10); // 日期
+ if (nd.equals("1")) {
+ sreturn = s_nd;
+ // 处理间隔符
+ if (format.equals("1"))
+ sreturn = sreturn + "年";
+ else if (format.equals("2"))
+ sreturn = sreturn + "-";
+ else if (format.equals("3"))
+ sreturn = sreturn + "/";
+ else if (format.equals("5"))
+ sreturn = sreturn + ".";
+ }
+ // 处理月份
+ if (yf.equals("1")) {
+ sreturn = sreturn + s_yf;
+ if (format.equals("1"))
+ sreturn = sreturn + "月";
+ else if (format.equals("2"))
+ sreturn = sreturn + "-";
+ else if (format.equals("3"))
+ sreturn = sreturn + "/";
+ else if (format.equals("5"))
+ sreturn = sreturn + ".";
+ }
+ // 处理日期
+ if (rq.equals("1")) {
+ sreturn = sreturn + s_rq;
+ if (format.equals("1"))
+ sreturn = sreturn + "日";
+ }
+ }
+ return sreturn;
+ }
+
+ public static String getNextMonthDay(String sdate, int m) {
+ sdate = getOKDate(sdate);
+ int year = Integer.parseInt(sdate.substring(0, 4));
+ int month = Integer.parseInt(sdate.substring(5, 7));
+ month = month + m;
+ if (month < 0) {
+ month = month + 12;
+ year = year - 1;
+ } else if (month > 12) {
+ month = month - 12;
+ year = year + 1;
+ }
+ String smonth = "";
+ if (month < 10)
+ smonth = "0" + month;
+ else
+ smonth = "" + month;
+ return year + "-" + smonth + "-10";
+ }
+
+ public static String getOKDate(String sdate) {
+ if (sdate == null || sdate.equals(""))
+ return getStringDateShort();
+
+ // 将“/”转换为“-”
+ // 如果只有8位长度,则要进行转换
+ if (sdate.length() == 8)
+ sdate = sdate.substring(0, 4) + "-" + sdate.substring(4, 6) + "-" + sdate.substring(6, 8);
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
+ ParsePosition pos = new ParsePosition(0);
+ Date strtodate = formatter.parse(sdate, pos);
+ String dateString = formatter.format(strtodate);
+ return dateString;
+ }
+
+ public static Calendar getCalendarFromDate(Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(date);
+ return cal;
+ }
+
+ public static void main(String[] args) throws Exception {
+ try {
+ //System.out.print(Integer.valueOf(getTwoDay("2006-11-03 12:22:10", "2006-11-02 11:22:09")));
+ } catch (Exception e) {
+ throw new Exception();
+ }
+ //System.out.println("sss");
+ }
+
+ public static String getDateTime(long ts) {
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ String str = df.format(ts);
+ return str;
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/util/ZipUtils.java b/qbaselib/src/main/java/com/quseit/util/ZipUtils.java
new file mode 100644
index 00000000..10b6b4e3
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/util/ZipUtils.java
@@ -0,0 +1,399 @@
+package com.quseit.util;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+import java.util.zip.ZipFile;
+import java.util.zip.ZipOutputStream;
+/**
+ * Java utils 实现的Zip工具
+ * @author miaowei
+ *
+ */
+public class ZipUtils {
+
+ private static final int BUFF_SIZE = 1024 * 1024; // 1M Byte
+
+ /**
+ * 批量压缩文件(夹)
+ *
+ * @param resFileList 要压缩的文件(夹)列表
+ * @param zipFile 生成的压缩文件
+ * @throws IOException 当压缩过程出错时抛出
+ */
+ public static void zipFiles(Collection resFileList, File zipFile) throws IOException {
+ ZipOutputStream zipout = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile), BUFF_SIZE));
+ for (File resFile : resFileList) {
+ zipFile(resFile, zipout, "");
+ }
+ zipout.close();
+ }
+
+ /**
+ * 批量压缩文件(夹)
+ *
+ * @param resFileList 要压缩的文件(夹)列表
+ * @param zipFile 生成的压缩文件
+ * @param comment 压缩文件的注释
+ * @throws IOException 当压缩过程出错时抛出
+ */
+ public static void zipFiles(Collection resFileList, File zipFile, String comment)
+ throws IOException {
+ ZipOutputStream zipout = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(
+ zipFile), BUFF_SIZE));
+ for (File resFile : resFileList) {
+ zipFile(resFile, zipout, "");
+ }
+ zipout.setComment(comment);
+ zipout.close();
+ }
+
+ /**
+ * 解压缩一个文件
+ *
+ * @param zipFile 压缩文件
+ * @param folderPath 解压缩的目标目录
+ * @throws IOException 当解压缩过程出错时抛出
+ */
+ public static void upZipFile(File zipFile, String folderPath) throws IOException {
+ File desDir = new File(folderPath);
+ if (!desDir.exists()) {
+ desDir.mkdirs();
+ }
+ ZipFile zf = new ZipFile(zipFile);
+ for (Enumeration> entries = zf.entries(); entries.hasMoreElements();) {
+ ZipEntry entry = ((ZipEntry)entries.nextElement());
+ if (entry.isDirectory()) {
+
+ continue;
+ }
+ InputStream in = zf.getInputStream(entry);
+ String str = folderPath + File.separator + entry.getName();
+ str = new String(str.getBytes(), "utf-8");
+ File desFile = new File(str);
+ if (!desFile.exists()) {
+ File fileParentDir = desFile.getParentFile();
+ if (!fileParentDir.exists()) {
+ fileParentDir.mkdirs();
+ }
+ desFile.createNewFile();
+ }
+ OutputStream out = new FileOutputStream(desFile);
+ byte buffer[] = new byte[BUFF_SIZE];
+ int realLength;
+ while ((realLength = in.read(buffer)) > 0) {
+ out.write(buffer, 0, realLength);
+ }
+ in.close();
+ out.close();
+ }
+ }
+
+ /**
+ * 解压文件名包含传入文字的文件
+ *
+ * @param zipFile 压缩文件
+ * @param folderPath 目标文件夹
+ * @param nameContains 传入的文件匹配名
+ * @throws ZipException 压缩格式有误时抛出
+ * @throws IOException IO错误时抛出
+ */
+ public static ArrayList upZipSelectedFile(File zipFile, String folderPath,
+ String nameContains) throws ZipException, IOException {
+ ArrayList fileList = new ArrayList();
+
+ File desDir = new File(folderPath);
+ if (!desDir.exists()) {
+ desDir.mkdir();
+ }
+
+ ZipFile zf = new ZipFile(zipFile);
+ for (Enumeration> entries = zf.entries(); entries.hasMoreElements();) {
+ ZipEntry entry = ((ZipEntry)entries.nextElement());
+ if (entry.getName().contains(nameContains)) {
+ InputStream in = zf.getInputStream(entry);
+ String str = folderPath + File.separator + entry.getName();
+ str = new String(str.getBytes("utf-8"), "gbk");
+ // str.getBytes("GB2312"),"8859_1" 输出
+ // str.getBytes("8859_1"),"GB2312" 输入
+ File desFile = new File(str);
+ if (!desFile.exists()) {
+ File fileParentDir = desFile.getParentFile();
+ if (!fileParentDir.exists()) {
+ fileParentDir.mkdirs();
+ }
+ desFile.createNewFile();
+ }
+ OutputStream out = new FileOutputStream(desFile);
+ byte buffer[] = new byte[BUFF_SIZE];
+ int realLength;
+ while ((realLength = in.read(buffer)) > 0) {
+ out.write(buffer, 0, realLength);
+ }
+ in.close();
+ out.close();
+ fileList.add(desFile);
+ }
+ }
+ return fileList;
+ }
+
+ /**
+ * 获得压缩文件内文件列表
+ *
+ * @param zipFile 压缩文件
+ * @return 压缩文件内文件名称
+ * @throws ZipException 压缩文件格式有误时抛出
+ * @throws IOException 当解压缩过程出错时抛出
+ */
+ public static ArrayList getEntriesNames(File zipFile) throws ZipException, IOException {
+ ArrayList entryNames = new ArrayList();
+ Enumeration> entries = getEntriesEnumeration(zipFile);
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = ((ZipEntry)entries.nextElement());
+ entryNames.add(new String(getEntryName(entry).getBytes("GB2312"), "8859_1"));
+ }
+ return entryNames;
+ }
+
+ /**
+ * 获得压缩文件内压缩文件对象以取得其属性
+ *
+ * @param zipFile 压缩文件
+ * @return 返回一个压缩文件列表
+ * @throws ZipException 压缩文件格式有误时抛出
+ * @throws IOException IO操作有误时抛出
+ */
+ public static Enumeration> getEntriesEnumeration(File zipFile) throws ZipException,
+ IOException {
+ ZipFile zf = new ZipFile(zipFile);
+ return zf.entries();
+
+ }
+
+ /**
+ * 取得压缩文件对象的注释
+ *
+ * @param entry 压缩文件对象
+ * @return 压缩文件对象的注释
+ * @throws UnsupportedEncodingException
+ */
+ public static String getEntryComment(ZipEntry entry) throws UnsupportedEncodingException {
+ return new String(entry.getComment().getBytes("GB2312"), "8859_1");
+ }
+
+ /**
+ * 取得压缩文件对象的名称
+ *
+ * @param entry 压缩文件对象
+ * @return 压缩文件对象的名称
+ * @throws UnsupportedEncodingException
+ */
+ public static String getEntryName(ZipEntry entry) throws UnsupportedEncodingException {
+ return new String(entry.getName().getBytes("GB2312"), "8859_1");
+ }
+
+ /**
+ * 压缩文件
+ *
+ * @param resFile 需要压缩的文件(夹)
+ * @param zipout 压缩的目的文件
+ * @param rootpath 压缩的文件路径
+ * @throws FileNotFoundException 找不到文件时抛出
+ * @throws IOException 当压缩过程出错时抛出
+ */
+ private static void zipFile(File resFile, ZipOutputStream zipout, String rootpath)
+ throws FileNotFoundException, IOException {
+ rootpath = rootpath + (rootpath.trim().length() == 0 ? "" : File.separator)
+ + resFile.getName();
+ rootpath = new String(rootpath.getBytes(), "utf-8");
+ if (resFile.isDirectory()) {
+ File[] fileList = resFile.listFiles();
+ for (File file : fileList) {
+ zipFile(file, zipout, rootpath);
+ }
+ } else {
+ byte buffer[] = new byte[BUFF_SIZE];
+ BufferedInputStream in = new BufferedInputStream(new FileInputStream(resFile),
+ BUFF_SIZE);
+ zipout.putNextEntry(new ZipEntry(rootpath));
+ int realLength;
+ while ((realLength = in.read(buffer)) != -1) {
+ zipout.write(buffer, 0, realLength);
+ }
+ in.close();
+ zipout.flush();
+ zipout.closeEntry();
+ }
+ }
+
+ //第二种实现
+ public static void zip(String src, String dest) throws IOException {
+ // 提供了一个数据项压缩成一个ZIP归档输出流
+ ZipOutputStream out = null;
+ try {
+
+ //DirTraversal.makeRootDirectory(dest);
+ //File outFile = DirTraversal.getFilePath(dest,"cache.zip");
+
+ File outFile = new File(dest);// 源文件或者目录
+ File fileOrDirectory = new File(src);// 压缩文件路径
+ out = new ZipOutputStream(new FileOutputStream(outFile));
+ // 如果此文件是一个文件,否则为false。
+ if (fileOrDirectory.isFile()) {
+ zipFileOrDirectory(out, fileOrDirectory, "");
+ } else {
+ // 返回一个文件或空阵列。
+ File[] entries = fileOrDirectory.listFiles();
+ for (int i = 0; i < entries.length; i++) {
+ // 递归压缩,更新curPaths
+ zipFileOrDirectory(out, entries[i], "");
+ }
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ } finally {
+ // 关闭输出流
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+ }
+
+ private static void zipFileOrDirectory(ZipOutputStream out,
+ File fileOrDirectory, String curPath) throws IOException {
+ // 从文件中读取字节的输入流
+ FileInputStream in = null;
+ try {
+ // 如果此文件是一个目录,否则返回false。
+ if (!fileOrDirectory.isDirectory()) {
+ // 压缩文件
+ byte[] buffer = new byte[4096];
+ int bytes_read;
+ in = new FileInputStream(fileOrDirectory);
+ // 实例代表一个条目内的ZIP归档
+ ZipEntry entry = new ZipEntry(curPath
+ + fileOrDirectory.getName());
+ // 条目的信息写入底层流
+ out.putNextEntry(entry);
+ while ((bytes_read = in.read(buffer)) != -1) {
+ out.write(buffer, 0, bytes_read);
+ }
+ out.closeEntry();
+ } else {
+ // 压缩目录
+ File[] entries = fileOrDirectory.listFiles();
+ for (int i = 0; i < entries.length; i++) {
+ // 递归压缩,更新curPaths
+ zipFileOrDirectory(out, entries[i], curPath
+ + fileOrDirectory.getName() + "/");
+ }
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ // throw ex;
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static void unzip(String zipFileName, String outputDirectory)
+ throws IOException {
+ ZipFile zipFile = null;
+ try {
+ zipFile = new ZipFile(zipFileName);
+ Enumeration e = zipFile.entries();
+ ZipEntry zipEntry = null;
+ File dest = new File(outputDirectory);
+ dest.mkdirs();
+ while (e.hasMoreElements()) {
+ zipEntry = (ZipEntry) e.nextElement();
+ String entryName = zipEntry.getName();
+ InputStream in = null;
+ FileOutputStream out = null;
+ try {
+ if (zipEntry.isDirectory()) {
+ String name = zipEntry.getName();
+ name = name.substring(0, name.length() - 1);
+ File f = new File(outputDirectory + File.separator
+ + name);
+ f.mkdirs();
+ } else {
+ int index = entryName.lastIndexOf("\\");
+ if (index != -1) {
+ File df = new File(outputDirectory + File.separator
+ + entryName.substring(0, index));
+ df.mkdirs();
+ }
+ index = entryName.lastIndexOf("/");
+ if (index != -1) {
+ File df = new File(outputDirectory + File.separator
+ + entryName.substring(0, index));
+ df.mkdirs();
+ }
+ File f = new File(outputDirectory + File.separator
+ + zipEntry.getName());
+ // f.createNewFile();
+ in = zipFile.getInputStream(zipEntry);
+ out = new FileOutputStream(f.getAbsoluteFile());
+ int c;
+ byte[] by = new byte[1024];
+ while ((c = in.read(by)) != -1) {
+ out.write(by, 0, c);
+ }
+ out.flush();
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ throw new IOException("解压失败:" + ex.toString());
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException ex) {
+ }
+ }
+ if (out != null) {
+ try {
+ out.close();
+ } catch (IOException ex) {
+ }
+ }
+ }
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ throw new IOException("解压失败:" + ex.toString());
+ } finally {
+ if (zipFile != null) {
+ try {
+ zipFile.close();
+ } catch (IOException ex) {
+ }
+ }
+ }
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/view/AdSlidShowView.java b/qbaselib/src/main/java/com/quseit/view/AdSlidShowView.java
new file mode 100644
index 00000000..103e2de1
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/view/AdSlidShowView.java
@@ -0,0 +1,340 @@
+package com.quseit.view;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import com.quseit.android.R;
+import com.quseit.util.ImageDownLoader;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class AdSlidShowView extends FrameLayout {
+ public urlBackcall adBackCall;
+ private String tag = "AdSlidShowView";
+ private android.support.v4.view.ViewPager viewPageAd;
+ private LinearLayout dotList;
+ private Context context;
+ private List adlistImage;
+ private int width;
+ private int currentItem = 0;
+ Handler handler = new Handler() {
+
+ public void handleMessage(android.os.Message msg) {
+ viewPageAd.setCurrentItem(currentItem);
+ }
+
+ ;
+ };
+ private ScheduledExecutorService scheduledExecutorService;
+
+ public AdSlidShowView(Context context) {
+ super(context);
+ this.context = context;
+ initView(context, null);
+ }
+
+ public AdSlidShowView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ this.context = context;
+ initView(context, attrs);
+ initViewPager();
+ startPlay();
+ }
+
+ public void stop() {
+ stopPlay();
+ }
+
+ public void setOnUrlBackCall(urlBackcall callBack) {
+ adBackCall = callBack;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ width = getMeasuredWidth();
+ setMeasuredDimension(width, width / 3);
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View v = getChildAt(i);
+ // this works because you set the dimensions of the ImageView to FILL_PARENT
+ v.measure(MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
+ }
+ }
+
+ /**
+ * 使用 List 设置轮播
+ *
+ * @param adlistImage
+ */
+ public void setAdImageList(List adlistImage) {
+ dotList.removeAllViews();
+ this.adlistImage = adlistImage;
+ setDotLists(this.adlistImage);
+ }
+
+ /**
+ * 加载视图 默认有 5个 轮播
+ *
+ * 1 设置setAdImageList 参数 List imgs 改变轮播
+ *
+ * 2 设置getAdListImage 参数 List Urls 改变轮播
+ *
+ * @param context
+ * @param attrs
+ */
+ private void initView(Context context, AttributeSet attrs) {
+
+ LayoutInflater.from(context).inflate(R.layout.ad_viewpage_view, this);
+ int[] resimg = new int[]{};
+ adlistImage = new ArrayList();
+
+ for (int i = 0; i < resimg.length; i++) {
+ ImageView imgView = new ImageView(context);
+ imgView.setImageResource(resimg[i]);
+ imgView.setScaleType(ImageView.ScaleType.FIT_XY);
+ adlistImage.add(imgView);
+ }
+
+ }
+
+ private void initViewPager() {
+ viewPageAd = (ViewPager) findViewById(R.id.ad_viewPager);
+ viewPageAd.setFocusable(true);
+ viewPageAd.setAdapter(new adviewPagerAdpter());
+ viewPageAd.setOnPageChangeListener(new adviewpagerListener());
+ dotList = (LinearLayout) findViewById(R.id.ll_dotparent);
+ }
+
+ private void startPlay() {
+ scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+ scheduledExecutorService.scheduleAtFixedRate(new SlidShowTask(), 1, 4,
+ TimeUnit.SECONDS);
+ }
+
+ private void stopPlay() {
+ scheduledExecutorService.shutdown();
+ }
+
+ public void setImagesFromUrl(List urls) {
+ //Log.d("BS", "setImageFromUrl:" + urls.toString());
+ dotList.removeAllViews();
+ this.adlistImage = getAdListImage(urls);
+ setDotLists(this.adlistImage);
+ ImageDownLoader loader = new ImageDownLoader(this.getContext());
+
+/*
+ Random rd=new Random();
+ int count=Math.abs(rd.nextInt()%2);
+ if(count==0){
+ List res=new ArrayList();
+ for(int i=urls.size()-1;i>=0;i--){
+ res.add(urls.get(i));
+ }
+ urls=res;
+ }
+*/
+
+ for (int i = 0; i < urls.size(); i++) {
+ String url = urls.get(i);
+ final int index = i;
+ Bitmap bitmap = loader.getBitmapCache(url);
+ if (bitmap != null) {
+ setImage(bitmap, i);
+ } else {
+ if (loader.getTaskCollection().containsKey(url)) {
+ return;
+ }
+
+ loader.loadImage(url, this.getWidth(), this.getHeight(),
+ new ImageDownLoader.AsyncImageLoaderListener() {
+
+ @Override
+ public void onImageLoader(Bitmap bitmap) {
+ if (bitmap != null) {
+ setImage(bitmap, index);
+ // Log.e("錯誤的", index+"");
+ }
+ }
+
+ });
+ }
+ }
+ }
+
+ /**
+ * 使用 List urls 设置轮播
+ *
+ * @param urls 链接集合
+ * @return
+ */
+ private List getAdListImage(List urls) {
+ List imgs = new ArrayList();
+ for (int i = 0; i < urls.size(); i++) {
+ ImageView view = new ImageView(context);
+// view.setBackgroundResource(R.drawable.splash_port);
+ view.setScaleType(ImageView.ScaleType.FIT_XY);
+ imgs.add(view);
+ }
+ return imgs;
+ }
+
+ public void setDotLists(List adlistImage) {
+
+ if (adlistImage.size() != 1) {
+ for (int i = 0; i < adlistImage.size(); i++) {
+ View v = new View(context);
+ if (i == 0)
+ v.setBackgroundResource(R.drawable.ic_spot_selected);
+ else
+ v.setBackgroundResource(R.drawable.ic_spot);
+ v.setLayoutParams(new LayoutParams(dp2px(10), dp2px(10)));
+ dotList.addView(v);
+ }
+ }
+ initViewPager();
+ }
+
+ private int dp2px(float dp) {
+ Resources r = Resources.getSystem();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
+ }
+
+ public void setImage(Bitmap bitmap, int i) {
+ ImageView view = adlistImage.get(i);
+ Drawable drawable = new BitmapDrawable(bitmap);
+ view.setImageDrawable(drawable);
+ requestLayout();
+ invalidate();
+ }
+
+ public interface urlBackcall {
+ void onUrlBackCall(int i);
+ }
+
+ private class adviewPagerAdpter extends PagerAdapter {
+ @Override
+ public int getCount() {
+ // Log.i(tag, "adviewPagerAdpter" + adlistImage.size());
+ return adlistImage.size();
+ }
+
+ @Override
+ public boolean isViewFromObject(View arg0, Object arg1) {
+ return arg0 == arg1;
+ }
+
+ @Override
+ public void destroyItem(View container, int position, Object object) {
+
+ }
+
+ @Override
+ public Object instantiateItem(View container, final int position) {
+ //Log.d("AdSlidShowView", "instantiateItem:"+adlistImage.size()+"-"+position);
+ if (adlistImage.size() != 0) {
+
+ try {
+ ((ViewGroup) container).addView(adlistImage.get(position % adlistImage.size()), 0);
+ } catch (Exception e) {
+
+ }
+
+ ImageView img = adlistImage.get(position % adlistImage.size());
+ img.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ adBackCall.onUrlBackCall(position % adlistImage.size());
+ }
+ });
+ return adlistImage.get(position % adlistImage.size());
+ } else {
+ return adlistImage.get(position % adlistImage.size());
+ }
+
+ }
+
+ }
+
+ private class adviewpagerListener implements OnPageChangeListener {
+ boolean isAutoPlay = false;
+
+ @Override
+ public void onPageScrollStateChanged(int arg0) {
+ //Log.d("AdSlidShowView", "onPageScrollStateChanged:"+arg0);
+ switch (arg0) {
+ case 1:
+ isAutoPlay = false;
+ stopPlay();
+ break;
+ case 2:
+ isAutoPlay = true;
+ break;
+ case 0:
+ if (viewPageAd.getCurrentItem() == viewPageAd.getAdapter()
+ .getCount() - 1 && !isAutoPlay) {
+ viewPageAd.setCurrentItem(0, false);
+ } else if (viewPageAd.getCurrentItem() == 0 && !isAutoPlay) {
+ viewPageAd.setCurrentItem(viewPageAd.getAdapter()
+ .getCount() - 1, false);
+ }
+
+ break;
+
+ }
+
+ }
+
+ @Override
+ public void onPageScrolled(int arg0, float arg1, int arg2) {
+
+ }
+
+ @Override
+ public void onPageSelected(int arg0) {
+ // Log.i(tag, "" + arg0);
+
+ currentItem = arg0;
+ for (int i = 0; i < dotList.getChildCount(); i++) {
+ if (i == arg0) {
+ ((View) dotList.getChildAt(i))
+ .setBackgroundResource(R.drawable.ic_spot_selected);
+ } else {
+ ((View) dotList.getChildAt(i))
+ .setBackgroundResource(R.drawable.ic_spot);
+ }
+ }
+ }
+ }
+
+ private class SlidShowTask implements Runnable {
+ public void run() {
+ synchronized (viewPageAd) {
+ currentItem = (currentItem + 1) % adlistImage.size();
+ handler.obtainMessage().sendToTarget();
+ }
+ }
+
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/view/SmallWindowView.java b/qbaselib/src/main/java/com/quseit/view/SmallWindowView.java
new file mode 100644
index 00000000..662fca96
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/view/SmallWindowView.java
@@ -0,0 +1,28 @@
+package com.quseit.view;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.RelativeLayout;
+
+import com.quseit.android.R;
+import com.quseit.config.BASE_CONF;
+import com.quseit.util.PreferenceUtil;
+
+
+public class SmallWindowView extends WindowView {
+
+ public SmallWindowView(Context context) {
+ super(context);
+ LayoutInflater.from(context).inflate(R.layout.float_window_small, this);
+ View view = findViewById(R.id.linLayoutSmall);
+ int statusBarHeight = PreferenceUtil.getSingleton(context).getInt(BASE_CONF.SP_STATUSBAR_HEIGHT, 0);
+ if (statusBarHeight != 0) {
+ RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) view.getLayoutParams();
+ layoutParams.height = statusBarHeight;
+ view.setLayoutParams(layoutParams);
+ }
+ viewWidth = view.getLayoutParams().width;
+ viewHeight = view.getLayoutParams().height;
+ }
+}
diff --git a/qbaselib/src/main/java/com/quseit/view/WindowView.java b/qbaselib/src/main/java/com/quseit/view/WindowView.java
new file mode 100644
index 00000000..b3c1a722
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/view/WindowView.java
@@ -0,0 +1,18 @@
+package com.quseit.view;
+
+import android.content.Context;
+import android.widget.RelativeLayout;
+
+/**
+ * Created by guojinyu on 2015/8/3.
+ */
+public class WindowView extends RelativeLayout {
+
+ public int viewWidth;
+ public int viewHeight;
+
+ public WindowView(Context context) {
+ super(context);
+ }
+
+}
diff --git a/qbaselib/src/main/java/com/quseit/view/item/VideoItem.java b/qbaselib/src/main/java/com/quseit/view/item/VideoItem.java
new file mode 100644
index 00000000..4389ed7f
--- /dev/null
+++ b/qbaselib/src/main/java/com/quseit/view/item/VideoItem.java
@@ -0,0 +1,102 @@
+package com.quseit.view.item;
+
+import java.io.IOException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import com.quseit.android.R;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ViewGroup;
+
+
+public class VideoItem {
+ private static final String TAG = "VideoItem";
+
+ public String headerText;
+ public String desc;
+ public String headUrl;
+ public String other;
+ public int statImage;
+ public int thumbImage;
+ public Bitmap coverBitmap;
+ public int tpl;
+
+ public VideoItem(String text, String desc) {
+ this.desc = desc;
+ this.headUrl = "";
+ this.other = "";
+ this.statImage = 0;
+ this.thumbImage = 0;
+ this.coverBitmap = null;
+ this.tpl = 0;
+ }
+
+ public VideoItem(String text, String desc, String headUrl, String other, int stat) {
+ this.desc = desc;
+ this.headUrl = headUrl;
+ this.other = other;
+ this.statImage = stat;
+ this.thumbImage = 0;
+ this.coverBitmap = null;
+ this.tpl = 0;
+
+
+ }
+ public VideoItem(String text, String desc, String headUrl, String other, int stat, int thumb) {
+ this.desc = desc;
+ this.headUrl = headUrl;
+ this.other = other;
+ this.statImage = stat;
+ this.thumbImage = thumb;
+ this.coverBitmap = null;
+ this.tpl = 0;
+
+
+ }
+
+ public VideoItem(String text, String desc, Bitmap coverBitmap, String other, int stat, int thumb) {
+ this.desc = desc;
+ this.coverBitmap = coverBitmap;
+ this.other = other;
+ this.statImage = stat;
+ this.thumbImage = thumb;
+ this.tpl = 0;
+
+ }
+
+ public VideoItem(String text, String desc, String headUrl, String other, int stat, int thumb, int tpl) {
+ this.desc = desc;
+ this.headUrl = headUrl;
+ this.other = other;
+ this.statImage = stat;
+ this.thumbImage = thumb;
+ this.coverBitmap = null;
+ this.tpl = tpl;
+
+
+ }
+
+
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException {
+ TypedArray a = r.obtainAttributes(attrs, R.styleable.VideoItem);
+
+ headerText = a.getString(R.styleable.VideoItem_headerText);
+ desc = a.getString(R.styleable.VideoItem_desc);
+ headUrl = a.getString(R.styleable.VideoItem_headUrl);
+ other = a.getString(R.styleable.VideoItem_other);
+ statImage = a.getInteger(R.styleable.VideoItem_statImage, statImage);
+ thumbImage = a.getInteger(R.styleable.VideoItem_thumbImage, thumbImage);
+ tpl = a.getInteger(R.styleable.VideoItem_tpl, tpl);
+
+ a.recycle();
+ Log.d(TAG, "inflate");
+ }
+
+}
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/alert_dialog_icon.png b/qbaselib/src/main/res/drawable-xxhdpi/alert_dialog_icon.png
new file mode 100644
index 00000000..fe54477c
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/alert_dialog_icon.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/btn_silver.9.png b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver.9.png
new file mode 100644
index 00000000..ef3067ab
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver.9.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_focus.9.png b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_focus.9.png
new file mode 100644
index 00000000..cf8e1194
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_focus.9.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_press.9.png b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_press.9.png
new file mode 100644
index 00000000..e9693322
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/btn_silver_press.9.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/horizontal_separation_line.png b/qbaselib/src/main/res/drawable-xxhdpi/horizontal_separation_line.png
new file mode 100644
index 00000000..fd5a8725
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/horizontal_separation_line.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_download_nb.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_download_nb.png
new file mode 100644
index 00000000..f7519cae
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_download_nb.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_email.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_email.png
new file mode 100644
index 00000000..d821bc93
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_email.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_email2.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_email2.png
new file mode 100644
index 00000000..6725a933
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_email2.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_popup_reminder.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_popup_reminder.png
new file mode 100644
index 00000000..4f2b82ce
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_popup_reminder.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_remove.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_remove.png
new file mode 100644
index 00000000..cde36e1f
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_remove.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_right.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_right.png
new file mode 100644
index 00000000..e6495b29
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_right.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_1.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_1.png
new file mode 100644
index 00000000..c329f58d
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_1.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_2.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_2.png
new file mode 100644
index 00000000..47ae1867
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_social_share_2.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_1.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_1.png
new file mode 100644
index 00000000..9e719c9c
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_1.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_2.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_2.png
new file mode 100644
index 00000000..8c1b4512
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_undo_2.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/ic_warning_nb.png b/qbaselib/src/main/res/drawable-xxhdpi/ic_warning_nb.png
new file mode 100644
index 00000000..1fefdd8b
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/ic_warning_nb.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/mini_right_tag.png b/qbaselib/src/main/res/drawable-xxhdpi/mini_right_tag.png
new file mode 100644
index 00000000..fc94a6f3
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/mini_right_tag.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/panel_bg.9.png b/qbaselib/src/main/res/drawable-xxhdpi/panel_bg.9.png
new file mode 100644
index 00000000..725a60df
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/panel_bg.9.png differ
diff --git a/qbaselib/src/main/res/drawable-xxhdpi/transparent.png b/qbaselib/src/main/res/drawable-xxhdpi/transparent.png
new file mode 100644
index 00000000..3411463c
Binary files /dev/null and b/qbaselib/src/main/res/drawable-xxhdpi/transparent.png differ
diff --git a/qbaselib/src/main/res/drawable/background_button.xml b/qbaselib/src/main/res/drawable/background_button.xml
new file mode 100644
index 00000000..62ba100b
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/background_button.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/background_button2.xml b/qbaselib/src/main/res/drawable/background_button2.xml
new file mode 100644
index 00000000..3e6ade9c
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/background_button2.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/drawable/background_button3.xml b/qbaselib/src/main/res/drawable/background_button3.xml
new file mode 100644
index 00000000..9b33fcf8
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/background_button3.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/drawable/background_button8.xml b/qbaselib/src/main/res/drawable/background_button8.xml
new file mode 100644
index 00000000..15eb4a2d
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/background_button8.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/drawable/background_button_cmain3.xml b/qbaselib/src/main/res/drawable/background_button_cmain3.xml
new file mode 100644
index 00000000..924da4e2
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/background_button_cmain3.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/float_bg.xml b/qbaselib/src/main/res/drawable/float_bg.xml
new file mode 100644
index 00000000..891b5d35
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/float_bg.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/drawable/ic_arrow_back_white.png b/qbaselib/src/main/res/drawable/ic_arrow_back_white.png
new file mode 100644
index 00000000..a2051cef
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_arrow_back_white.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_delete.png b/qbaselib/src/main/res/drawable/ic_delete.png
new file mode 100644
index 00000000..e9ce89e0
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_delete.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_error_nb.png b/qbaselib/src/main/res/drawable/ic_error_nb.png
new file mode 100644
index 00000000..8ed186b4
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_error_nb.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_feedback.xml b/qbaselib/src/main/res/drawable/ic_feedback.xml
new file mode 100644
index 00000000..b3b59ba4
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/ic_feedback.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/ic_go.png b/qbaselib/src/main/res/drawable/ic_go.png
new file mode 100644
index 00000000..e70f0413
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_go.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_launcher.png b/qbaselib/src/main/res/drawable/ic_launcher.png
new file mode 100644
index 00000000..b7710205
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_launcher.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_pause.png b/qbaselib/src/main/res/drawable/ic_pause.png
new file mode 100644
index 00000000..9661cfbb
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_pause.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_plugin.png b/qbaselib/src/main/res/drawable/ic_plugin.png
new file mode 100644
index 00000000..ae138edb
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_plugin.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_right_a.xml b/qbaselib/src/main/res/drawable/ic_right_a.xml
new file mode 100644
index 00000000..69417766
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/ic_right_a.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/ic_setting.png b/qbaselib/src/main/res/drawable/ic_setting.png
new file mode 100644
index 00000000..3e4580e0
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_setting.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_social_share.xml b/qbaselib/src/main/res/drawable/ic_social_share.xml
new file mode 100644
index 00000000..96d8c278
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/ic_social_share.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/ic_spot.png b/qbaselib/src/main/res/drawable/ic_spot.png
new file mode 100644
index 00000000..6b3b0e20
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_spot.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_spot_selected.png b/qbaselib/src/main/res/drawable/ic_spot_selected.png
new file mode 100644
index 00000000..17d365c3
Binary files /dev/null and b/qbaselib/src/main/res/drawable/ic_spot_selected.png differ
diff --git a/qbaselib/src/main/res/drawable/ic_undo.xml b/qbaselib/src/main/res/drawable/ic_undo.xml
new file mode 100644
index 00000000..73002e36
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/ic_undo.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/drawable/icon_nb.png b/qbaselib/src/main/res/drawable/icon_nb.png
new file mode 100644
index 00000000..3411463c
Binary files /dev/null and b/qbaselib/src/main/res/drawable/icon_nb.png differ
diff --git a/qbaselib/src/main/res/drawable/silver_button.xml b/qbaselib/src/main/res/drawable/silver_button.xml
new file mode 100644
index 00000000..e29115f4
--- /dev/null
+++ b/qbaselib/src/main/res/drawable/silver_button.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/qpython/src/main/res/drawable/purchase_bg.xml b/qbaselib/src/main/res/drawable/trans_bg.xml
similarity index 52%
rename from qpython/src/main/res/drawable/purchase_bg.xml
rename to qbaselib/src/main/res/drawable/trans_bg.xml
index 63e66340..6de5f0ba 100644
--- a/qpython/src/main/res/drawable/purchase_bg.xml
+++ b/qbaselib/src/main/res/drawable/trans_bg.xml
@@ -1,6 +1,5 @@
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/layout-sw360dp/float_window_small.xml b/qbaselib/src/main/res/layout-sw360dp/float_window_small.xml
new file mode 100644
index 00000000..71c7b343
--- /dev/null
+++ b/qbaselib/src/main/res/layout-sw360dp/float_window_small.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/layout/ad_viewpage_view.xml b/qbaselib/src/main/res/layout/ad_viewpage_view.xml
new file mode 100644
index 00000000..562aa444
--- /dev/null
+++ b/qbaselib/src/main/res/layout/ad_viewpage_view.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/layout/m_ftp_setting.xml b/qbaselib/src/main/res/layout/m_ftp_setting.xml
new file mode 100644
index 00000000..1613ed08
--- /dev/null
+++ b/qbaselib/src/main/res/layout/m_ftp_setting.xml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/layout/opt_prompt_1_input.xml b/qbaselib/src/main/res/layout/opt_prompt_1_input.xml
new file mode 100644
index 00000000..7ea06a32
--- /dev/null
+++ b/qbaselib/src/main/res/layout/opt_prompt_1_input.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/layout/opt_prompt_2_btn.xml b/qbaselib/src/main/res/layout/opt_prompt_2_btn.xml
new file mode 100644
index 00000000..18138a79
--- /dev/null
+++ b/qbaselib/src/main/res/layout/opt_prompt_2_btn.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/layout/opt_prompt_2_input.xml b/qbaselib/src/main/res/layout/opt_prompt_2_input.xml
new file mode 100644
index 00000000..deb93d23
--- /dev/null
+++ b/qbaselib/src/main/res/layout/opt_prompt_2_input.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/values-zh-rCN/arrays.xml b/qbaselib/src/main/res/values-zh-rCN/arrays.xml
new file mode 100644
index 00000000..e8bf33d0
--- /dev/null
+++ b/qbaselib/src/main/res/values-zh-rCN/arrays.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/qbaselib/src/main/res/values-zh-rCN/strings.xml b/qbaselib/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..9d8529dc
--- /dev/null
+++ b/qbaselib/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,111 @@
+
+
+ QApplication
+ http://quseit.com/
+ 检查新版本
+ 升级
+ 发现新版本
+ 已经是最新版本
+ 开始下载
+ 开始更新
+ 正在下载
+ 下载成功
+ 在下载过程中发生了一个错误
+ 在更新过程中发生了一个错误
+ OK
+ 取消
+ 没有此功能
+ 关于
+ 赞助商广告
+ 载入
+ 标题
+ 内容
+ 隐藏
+ 不
+ 关闭
+ 取消
+ 对话框
+ 请选择 ...
+ 确认 ?
+ {0} {1} 正在下载,您想重新下载?
+ {0} {1} 已存在,您想重新下载?
+ 发生了一个错误,请检测您的网络连接
+ 暂停
+ 分享
+ 土拨鼠视频下载器 可以帮助您在 YouTube中找到并下载有趣的视频,这将是您在移动设备上观看 YouTube 上的视频的最好方法。
+ 隐私政策
+ http://tubebook.net/privacy.html
+ 选择
+ 无法找到,重新搜索?
+ 文件不存在
+
+ {0}成功下载
+ 相关
+
+ {0} 被取消
+ {0} 被暂停
+ 当前视频无法下载, 请稍后再试
+
+ FTP 服务
+ 升级为专业版
+ 代理设置
+ 代理主机
+ 代理端口
+ 返回
+ 分享
+ {0}
+ 请插入 SD 卡
+ 端口应该是整数
+ 非法的 IP 地址
+ 反馈
+ 无法连接, 请检查
+ 关于
+ 更多
+ 检查新版本
+ 脚本插件
+ 发生异常,已生成错误报告 {0}
+ 优化播放器编解码器
+ 发生错误,请重试
+ SL4A 服务
+ FTP 服务管理
+ FTP 服务设置
+ FTP 账户设置
+ FTP根目录:{0}
+ 帐户
+ 用户名
+ 密码
+ 端口
+ 开始 FTP 服务
+ 停止 FTP 服务
+ 库管理
+ 发送邮件
+ [Feedback]{0} (code:{1}) from:{2}
+ [Feedback]: \n\n[设备信息]\n{0},android:{1},sdk:{2} \n\n[最近错误信息]\n{3}\n\n[您的反馈]: \n{4}\n
+ 请输入关键字
+ 请输入网址
+ 默认根目录
+ 请输入默认根目录
+ 需要设定一个文件夹作为根目录
+ 根目录不存在
+ 成功,它将在下一个开始工作
+ QPython 插件
+ 为它打分
+ 成功
+ 得到帮助
+ 遇到麻烦了?
+ 浏览它的主页
+ 退出
+ 媒体中心
+ 默认程序
+ 升级到专业版
+ 重置内置空间
+ 端口
+ 警告
+ FTP服务器
+ 不能恢复地址
+ FTP 服务器运行在 %s
+ 启动 FTP 服务器
+ 未能启动 FTP 服务器
+ 异常:
+ 资源已失效,请尝试重启
+
diff --git a/qbaselib/src/main/res/values/arrays.xml b/qbaselib/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..e8bf33d0
--- /dev/null
+++ b/qbaselib/src/main/res/values/arrays.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/qbaselib/src/main/res/values/attrs.xml b/qbaselib/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..744d5d51
--- /dev/null
+++ b/qbaselib/src/main/res/values/attrs.xml
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qbaselib/src/main/res/values/colors.xml b/qbaselib/src/main/res/values/colors.xml
new file mode 100644
index 00000000..1731b8ab
--- /dev/null
+++ b/qbaselib/src/main/res/values/colors.xml
@@ -0,0 +1,25 @@
+
+
+ #FF4A4A4A
+ #FFE8E8E8
+ #00FFFFFF
+ #FF9B9B9B
+ #1f1f1f
+ #000000
+ #FF4BAC07
+ #FF363636
+
+ #FF0066CC
+
+ #c8c800
+
+ #2eb3e4
+
+ #ffffff
+ #fbfbfb
+
+ #e8e8e8
+
+ #ffffff
+ #000000
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/values/dimensions.xml b/qbaselib/src/main/res/values/dimensions.xml
new file mode 100644
index 00000000..35f48416
--- /dev/null
+++ b/qbaselib/src/main/res/values/dimensions.xml
@@ -0,0 +1,6 @@
+
+
+
+
+ 32dp
+
diff --git a/qbaselib/src/main/res/values/mraid_attrs.xml b/qbaselib/src/main/res/values/mraid_attrs.xml
new file mode 100644
index 00000000..f303d563
--- /dev/null
+++ b/qbaselib/src/main/res/values/mraid_attrs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qbaselib/src/main/res/values/strings.xml b/qbaselib/src/main/res/values/strings.xml
new file mode 100644
index 00000000..5df06bb1
--- /dev/null
+++ b/qbaselib/src/main/res/values/strings.xml
@@ -0,0 +1,111 @@
+
+
+ QPY
+ http://qpython.com/
+ Check for new version
+ Update
+ New version found
+ Already the newest version
+ Start to download …
+ Start to update …
+ Downloading …
+ Success update
+ An error occurs during download
+ An error occurs during update
+ OK
+ Cancel
+ No such feature
+ About
+ Sponsored ads
+ Loading …
+ Title
+ Content
+ Hide
+ No
+ Close
+ Cancel
+ Dialog box
+ Please choose …
+ Confirm ?
+ {0} {1} is downloading, are you sure to re-download now?
+ {0} {1} exists, do you want to re-download it?
+ There was an error, please check your network connection
+ Paused
+ Share
+ Tube Downloader can help you find and download interesting videos from YouTube, It\'s the best way you enjoy the videos of YouTube on mobile
+ Privacy Policy
+ http://quseit.com/privacy.html
+ Choose
+ Not found, try again?
+ The file did not exist
+ {0} was download successfully
+ About
+
+ {0} was cancled
+ {0} was paused
+ You could not download this video now, please retry later
+
+ FTP Service
+ Upgrade to Pro version
+ Proxy Setting
+ Proxy host
+ Proxy port
+ Back
+ Share
+ {0}
+ Please insert SD Card first
+ Port should be integer
+ Illegal IP Address
+ Feedback
+ No connection, please check it
+ About
+ More
+ Check for update
+ Script Plugin
+ An exception occured, error report {0} is genereated
+ Optimized Player Codec
+ Error occurred, try again
+ SL4A Service
+ FTP Service Manage
+ FTP Service Setting
+ FTP Account Setting
+ FTP Root Directory: {0}
+ Account
+ Username
+ Password
+ Port
+ Start FTP Service
+ Stop FTP Service
+ Libraries manage
+ Send Email
+ [Feedback]{0} (code:{1}) from:{2}
+ [Feedback]: \n\n[Device information]\n{0},android:{1},sdk:{2} \n\n[Recent error information]\n{3}\n\n[Or write your feedback here]: \n{4}\n
+ Please input the keyword
+ Please input the URL
+ Default Root
+ Please input Default Root
+ Root needs to be directory
+ Root does not exist
+ Success, It will work on next start
+ QPython Plugins
+ Rate it
+ Success
+ Get help
+ Got some trouble?
+
+ Exit
+ Media center
+ Default Program
+ Upgrade to Pro edition
+ Reset private space
+ port
+ Alert
+ FTP Server
+ Can\'t retrieve url
+ FTP server running at %s
+ Start FTP server
+ Failed to start the FTP server
+ exception:
+
+ Resource is expired,please re-download
+
diff --git a/qbaselib/src/main/res/values/styles.xml b/qbaselib/src/main/res/values/styles.xml
new file mode 100644
index 00000000..c56aad26
--- /dev/null
+++ b/qbaselib/src/main/res/values/styles.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qftplib b/qftplib
deleted file mode 160000
index c1ab9384..00000000
--- a/qftplib
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit c1ab93848ec77ba6670332acff1e712a3c00301b
diff --git a/qftplib/.gitignore b/qftplib/.gitignore
new file mode 100644
index 00000000..a166d227
--- /dev/null
+++ b/qftplib/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+build
+__MACOSX
+.idea
+keystore
+.DS_Store
+local.properties
+*.apk
diff --git a/qftplib/LICENSE b/qftplib/LICENSE
new file mode 100644
index 00000000..cd7b044d
--- /dev/null
+++ b/qftplib/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software code code, documentation
+ code, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, code code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ from_content syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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.
diff --git a/qftplib/README.md b/qftplib/README.md
new file mode 100644
index 00000000..6a56563f
--- /dev/null
+++ b/qftplib/README.md
@@ -0,0 +1,4 @@
+# About
+QFtplib provides basic ftp features for Android project.
+
+It is not a standalone project, it will be included by QPython / QPython3 as a submodule
diff --git a/qftplib/build.gradle b/qftplib/build.gradle
new file mode 100644
index 00000000..c2949f86
--- /dev/null
+++ b/qftplib/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ dependencies {
+ api 'com.android.support:documentfile:28.0.0'
+ }
+}
diff --git a/qftplib/src/main/AndroidManifest.xml b/qftplib/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c3b0df87
--- /dev/null
+++ b/qftplib/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/qftplib/src/main/java/org/swiftp/Defaults.java b/qftplib/src/main/java/org/swiftp/Defaults.java
new file mode 100644
index 00000000..bf8b141c
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/Defaults.java
@@ -0,0 +1,153 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp;
+
+import android.content.Context;
+import android.util.Log;
+
+public class Defaults {
+ protected static int inputBufferSize = 256;
+ public static int dataChunkSize = 65536; // do file I/O in 64k chunks
+ protected static int sessionMonitorScrollBack = 10;
+ protected static int serverLogScrollBack = 10;
+ protected static int uiLogLevel = Defaults.release ? Log.INFO : Log.DEBUG;
+ protected static int consoleLogLevel = Defaults.release ? Log.INFO : Log.DEBUG;
+ protected static String settingsName = "SwiFTP";
+ //protected static String username = "user";
+ //protected static String password = "";
+ protected static int portNumber = 0;//2121;
+// protected static int ipRetrievalAttempts = 5;
+ public static final int tcpConnectionBacklog = 5;
+ public static final String chrootDir = "/";
+ public static final boolean acceptWifi = true;
+ public static final boolean acceptNet = false; // don't incur bandwidth charges
+ public static final boolean stayAwake = true;
+ public static final int REMOTE_PROXY_PORT = 2222;
+ public static final String STRING_ENCODING = "UTF-8";
+ public static final int SO_TIMEOUT_MS = 30000; // socket timeout millis
+ // FTP control sessions should start out in ASCII, according to the RFC.
+ // However, many clients don't turn on UTF-8 even though they support it,
+ // so we just turn it on by default.
+ public static final String SESSION_ENCODING = "UTF-8";
+
+ // This is a flag that should be true for public builds and false for dev builds
+ public static final boolean release = true;
+
+ // Try to fix the transfer stall bug, reopen the destination file periodically
+ //public static final boolean do_reopen_hack = false;
+ //public static final int bytes_between_reopen = 4000000;
+
+ // Try to fix the transfer stall bug, flush the file periodically
+ //public static final boolean do_flush_hack = false;
+ //public static final int bytes_between_flush = 500000;
+
+ public static final boolean do_mediascanner_notify = true;
+
+
+// public static int getIpRetrievalAttempts() {
+// return ipRetrievalAttempts;
+// }
+
+// public static void setIpRetrievalAttempts(int ipRetrievalAttempts) {
+// Defaults.ipRetrievalAttempts = ipRetrievalAttempts;
+// }
+
+ public static int getPortNumber() {
+ return portNumber;
+ }
+
+ public static void setPortNumber(int portNumber) {
+ Defaults.portNumber = portNumber;
+ }
+
+ public static String getSettingsName() {
+ return settingsName;
+ }
+
+ public static void setSettingsName(String settingsName) {
+ Defaults.settingsName = settingsName;
+ }
+
+ public static int getSettingsMode() {
+ return settingsMode;
+ }
+
+ public static void setSettingsMode(int settingsMode) {
+ Defaults.settingsMode = settingsMode;
+ }
+
+ public static void setServerLogScrollBack(int serverLogScrollBack) {
+ Defaults.serverLogScrollBack = serverLogScrollBack;
+ }
+
+ protected static int settingsMode = Context.MODE_WORLD_WRITEABLE;
+
+ public static int getUiLogLevel() {
+ return uiLogLevel;
+ }
+
+ public static void setUiLogLevel(int uiLogLevel) {
+ Defaults.uiLogLevel = uiLogLevel;
+ }
+
+ public static int getInputBufferSize() {
+ return inputBufferSize;
+ }
+
+ public static void setInputBufferSize(int inputBufferSize) {
+ Defaults.inputBufferSize = inputBufferSize;
+ }
+
+ public static int getDataChunkSize() {
+ return dataChunkSize;
+ }
+
+ public static void setDataChunkSize(int dataChunkSize) {
+ Defaults.dataChunkSize = dataChunkSize;
+ }
+
+ public static int getSessionMonitorScrollBack() {
+ return sessionMonitorScrollBack;
+ }
+
+ public static void setSessionMonitorScrollBack(
+ int sessionMonitorScrollBack)
+ {
+ Defaults.sessionMonitorScrollBack = sessionMonitorScrollBack;
+ }
+
+ public static int getServerLogScrollBack() {
+ return serverLogScrollBack;
+ }
+
+ public static void setLogScrollBack(int serverLogScrollBack) {
+ Defaults.serverLogScrollBack = serverLogScrollBack;
+ }
+
+ public static int getConsoleLogLevel() {
+ return consoleLogLevel;
+ }
+
+ public static void setConsoleLogLevel(int consoleLogLevel) {
+ Defaults.consoleLogLevel = consoleLogLevel;
+ }
+
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/FTPServerService.java b/qftplib/src/main/java/org/swiftp/FTPServerService.java
new file mode 100644
index 00000000..c2cfd596
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/FTPServerService.java
@@ -0,0 +1,720 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+ */
+
+package org.swiftp;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.ServerSocket;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.swiftp.server.ProxyConnector;
+import org.swiftp.server.SessionThread;
+import org.swiftp.server.TcpListener;
+
+import android.annotation.SuppressLint;
+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.graphics.Bitmap;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.WifiLock;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+public abstract class FTPServerService extends Service implements Runnable {
+
+ // Service will broadcast (LocalBroadcast) when server start/stop
+ static public final String ACTION_STARTED = "org.swiftp.FTPServerService.STARTED";
+ static public final String ACTION_STOPPED = "org.swiftp.FTPServerService.STOPPED";
+ static public final String ACTION_FAILEDTOSTART = "org.swiftp.FTPServerService.FAILEDTOSTART";
+ public static final int BACKLOG = 21;
+ public static final int MAX_SESSIONS = 5;
+ public static final String WAKE_LOCK_TAG = "SwiFTP";
+ // The server thread will check this often to look for incoming
+ // connections. We are forced to use non-blocking accept() and polling
+ // because we cannot wait forever in accept() if we want to be able
+ // to receive an exit signal and cleanly exit.
+ public static final int WAKE_INTERVAL_MS = 1000; // milliseconds
+ private static final int WIFI_AP_STATE_ENABLED = 13;
+ protected static Thread serverThread = null;
+ protected static MyLog staticLog = new MyLog(FTPServerService.class.getName());
+ protected static WifiLock wifiLock = null;
+ protected static List sessionMonitor = new ArrayList();
+ protected static List serverLog = new ArrayList();
+
+ // protected static InetAddress serverAddress = null;
+ protected static int uiLogLevel = Defaults.getUiLogLevel();
+ protected static int port;
+ protected static boolean acceptWifi;
+ protected static boolean acceptNet;
+ protected static boolean fullWake;
+ private static SharedPreferences settings = null;
+ private final List sessionThreads = new ArrayList();
+ protected boolean shouldExit = false;
+ protected MyLog myLog = new MyLog(getClass().getName());
+ // protected ServerSocketChannel wifiSocket;
+ protected ServerSocket listenSocket;
+ NotificationManager notificationMgr = null;
+ PowerManager.WakeLock wakeLock;
+ private TcpListener wifiListener = null;
+ private ProxyConnector proxyConnector = null;
+
+ public FTPServerService() {
+ }
+
+ public static boolean isRunning() {
+ // return true if and only if a server Thread is running
+ if (serverThread == null) {
+ staticLog.l(Log.DEBUG, "Server is not running (null serverThread)");
+ return false;
+ }
+ if (!serverThread.isAlive()) {
+ staticLog.l(Log.DEBUG, "serverThread non-null but !isAlive()");
+ } else {
+ staticLog.l(Log.DEBUG, "Server is alive");
+ }
+ return true;
+ }
+
+ /**
+ * Gets the IP address of the wifi connection.
+ *
+ * @return The integer IP address if wifi enabled, or null if not.
+ */
+ public static InetAddress getWifiIp() {
+ Context myContext = Globals.getContext().getApplicationContext();
+ if (myContext == null) {
+ throw new NullPointerException("Global context is null");
+ }
+ WifiManager wifiMgr = (WifiManager) myContext.getSystemService(Context.WIFI_SERVICE);
+ if (isWifiEnabled()) {
+ int ipAsInt = wifiMgr.getConnectionInfo().getIpAddress();
+ if (ipAsInt == 0) {
+ return null;
+ } else {
+ return Util.intToInet(ipAsInt);
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public static ArrayList getWifiAndApIp(){
+ ArrayList ip = new ArrayList<>();
+ InetAddress addrWifi = getWifiIp();
+ String hostAddr;
+ if(addrWifi!=null)
+ ip.add(addrWifi.getHostAddress());
+ try {
+ for (NetworkInterface intf : Collections.list(NetworkInterface.getNetworkInterfaces())) {
+ for (InetAddress addr : Collections.list(intf.getInetAddresses())) {
+ hostAddr = addr.getHostAddress();
+ if(hostAddr == null || addr.isLoopbackAddress() || ip.contains(hostAddr))
+ continue;
+ if (hostAddr.contains(".")){
+ ip.add(hostAddr);
+ break;
+ }
+ }
+ }
+ } catch (SocketException ignored) {
+ }
+ if(ip.size()==0)
+ ip = null;
+ return ip;
+ }
+
+ public static String[] getIpPortString(){
+ ArrayList address = getWifiAndApIp();
+ if(address == null)
+ return null;
+ String[] ipPort = new String[address.size()];
+ for(int i = 0; i getSessionMonitorContents() {
+ return new ArrayList(sessionMonitor);
+ }
+
+ public static List getServerLogContents() {
+ return new ArrayList(serverLog);
+ }
+
+ public static void log(int msgLevel, String s) {
+ serverLog.add(s);
+ int maxSize = Defaults.getServerLogScrollBack();
+ while (serverLog.size() > maxSize) {
+ serverLog.remove(0);
+ }
+ // updateClients();
+ }
+
+ public static void writeMonitor(boolean incoming, String s) {
+ }
+
+ public static int getPort() {
+ return port;
+ }
+
+ public static void setPort(int port) {
+ FTPServerService.port = port;
+ }
+
+ static public SharedPreferences getSettings() {
+ return settings;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ // We don't implement this functionality, so ignore it
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ myLog.l(Log.DEBUG, "SwiFTP server created");
+ // Set the application-wide context global, if not already set
+ Context myContext = Globals.getContext();
+ if (myContext == null) {
+ myContext = getApplicationContext();
+ if (myContext != null) {
+ Globals.setContext(myContext);
+ }
+ }
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ super.onStart(intent, startId);
+
+ shouldExit = false;
+ int attempts = 10;
+ // The previous server thread may still be cleaning up, wait for it
+ // to finish.
+ while (serverThread != null) {
+ myLog.l(Log.WARN, "Won't start, server thread exists");
+ if (attempts > 0) {
+ attempts--;
+ Util.sleepIgnoreInterupt(1000);
+ } else {
+ myLog.l(Log.ERROR, "Server thread already exists");
+ return;
+ }
+ }
+ myLog.l(Log.DEBUG, "Creating server thread");
+ serverThread = new Thread(this);
+ serverThread.start();
+ }
+
+ @Override
+ public void onDestroy() {
+ myLog.l(Log.INFO, "onDestroy() Stopping server");
+ shouldExit = true;
+ if (serverThread == null) {
+ myLog.l(Log.WARN, "Stopping with null serverThread");
+ return;
+ } else {
+ serverThread.interrupt();
+ try {
+ serverThread.join(10000); // wait 10 sec for server thread to
+ // finish
+ } catch (InterruptedException e) {
+ }
+ if (serverThread.isAlive()) {
+ myLog.l(Log.WARN, "Server thread failed to exit");
+ // it may still exit eventually if we just leave the
+ // shouldExit flag set
+ } else {
+ myLog.d("serverThread join()ed ok");
+ serverThread = null;
+ }
+ }
+ try {
+ if (listenSocket != null) {
+ myLog.l(Log.INFO, "Closing listenSocket");
+ listenSocket.close();
+ }
+ } catch (IOException e) {
+ }
+
+ if (wifiLock != null) {
+ wifiLock.release();
+ wifiLock = null;
+ }
+ clearNotification();
+ myLog.d("FTPServerService.onDestroy() finished");
+ }
+
+ private boolean loadSettings() {
+ myLog.l(Log.DEBUG, "Loading settings");
+ loadPort(this);
+
+ myLog.l(Log.DEBUG, "Using port " + port);
+
+ acceptNet = settings.getBoolean("allowNet", Defaults.acceptNet);
+ acceptWifi = settings.getBoolean("allowWifi", Defaults.acceptWifi);
+ fullWake = settings.getBoolean(getString(R.string.key_stay_awake), Defaults.stayAwake);
+
+ // The username, password, and chrootDir are just checked for sanity
+ /*String username = settings.getString("username", null);
+ String password = settings.getString("password", null);
+ String chrootDir = settings.getString("chrootDir", Defaults.chrootDir);
+ */
+
+ String username = settings.getString(getString(R.string.key_username),"");
+ if (username.equals("")) {
+ username = Util.getCode(this);
+ }
+ String password = settings.getString(getString(R.string.key_ftp_pwd),"");
+ if (password.equals("")) {
+ password = Util.getCode(this);
+ }
+ String chrootDir = settings.getString(getString(R.string.key_root_dir),"");
+ if (chrootDir.equals("")) {
+ chrootDir = "/";
+ }
+ Log.d("FTPService", "(username):"+username+"(pwd)"+password+"(chroot)"+chrootDir);
+
+ validateBlock: {
+ if (username == null || password == null) {
+ myLog.l(Log.ERROR, "Username or password is invalid");
+ break validateBlock;
+ }
+ File chrootDirAsFile = new File(chrootDir);
+ if (!chrootDirAsFile.isDirectory()) {
+ myLog.l(Log.ERROR, "Chroot dir is invalid");
+ break validateBlock;
+ }
+
+
+ Globals.setChrootDir(chrootDirAsFile);
+ Globals.setUsername(username);
+ return true;
+ }
+ // We reach here if the settings were not sane
+ return false;
+ }
+
+ public static void loadPort(Context context){
+ settings = PreferenceManager.getDefaultSharedPreferences(context);
+ //port = Integer.valueOf(settings.getString("portNum", "2121"));
+ String portS = settings.getString(context.getString(R.string.key_port_num),"");
+ if (!portS.equals("")) {
+ port = Integer.valueOf(portS);
+ } else {
+ port = Defaults.portNumber;
+ }
+ }
+
+ // This opens a listening socket on all interfaces.
+ void setupListener() throws IOException {
+ listenSocket = new ServerSocket();
+ listenSocket.setReuseAddress(true);
+ listenSocket.bind(new InetSocketAddress(port));
+ }
+
+ private void setupNotification() {
+ // http://developer.android.com/guide/topics/ui/notifiers/notifications.html
+
+ // Get NotificationManager reference
+ String ns = Context.NOTIFICATION_SERVICE;
+ notificationMgr = (NotificationManager) getSystemService(ns);
+
+ // Instantiate a Notification
+ int smallIconId = R.drawable.ftp_notification;
+ //Bitmap largeIconId = R.drawable.ftp_notification;
+ CharSequence tickerText = getString(R.string.notif_server_starting);
+ long when = System.currentTimeMillis();
+
+
+ CharSequence contentTitle = getString(R.string.notif_title);
+ CharSequence contentText = getString(R.string.notif_text);
+ Intent notificationIntent = new Intent(this, getSettingClass());
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ notificationIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ Notification notification;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ notification = new Notification.Builder(this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(smallIconId)
+ //.setLargeIcon(largeIconId)
+ .setAutoCancel(false)
+ .setContentIntent(contentIntent)
+ .build();
+
+ } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB){
+ notification = new Notification.Builder(this)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSmallIcon(smallIconId)
+ .setSmallIcon(smallIconId)
+ //.setLargeIcon(largeIconId)
+ .setAutoCancel(false)
+ .setContentIntent(contentIntent)
+ .getNotification();
+
+ } else {
+ notification = new Notification(smallIconId, tickerText, when);
+ notification.contentIntent = contentIntent;
+ notification.tickerText = contentTitle;
+ notification.flags |= Notification.FLAG_ONGOING_EVENT;
+ }
+
+ notificationMgr.notify(0, notification);
+
+
+ myLog.d("Notication setup done");
+ }
+
+ private void clearNotification() {
+ if (notificationMgr == null) {
+ // Get NotificationManager reference
+ String ns = Context.NOTIFICATION_SERVICE;
+ notificationMgr = (NotificationManager) getSystemService(ns);
+ }
+ notificationMgr.cancelAll();
+ myLog.d("Cleared notification");
+ }
+
+ public void run() {
+ // The UI will want to check the server status to update its
+ // start/stop server button
+ int consecutiveProxyStartFailures = 0;
+ long proxyStartMillis = 0;
+
+ myLog.l(Log.DEBUG, "Server thread running");
+
+ // set our members according to user preferences
+ if (!loadSettings()) {
+ // loadSettings returns false if settings are not sane
+ cleanupAndStopService();
+ sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
+ return;
+ }
+
+ if (!isWifiAndApEnabled()) {
+ cleanupAndStopService();
+ sendBroadcast(new Intent(ACTION_FAILEDTOSTART));
+ return;
+ }
+
+ // Initialization of wifi
+ if (acceptWifi) {
+ // If configured to accept connections via wifi, then set up the
+ // socket
+ try {
+ setupListener();
+ } catch (IOException e) {
+ myLog.l(Log.WARN, "Error opening port, check your network connection.");
+ // serverAddress = null;
+ cleanupAndStopService();
+ return;
+ }
+ takeWifiLock();
+ }
+ takeWakeLock();
+
+ myLog.l(Log.INFO, "SwiFTP server ready");
+ setupNotification();
+
+ // A socket is open now, so the FTP server is started, notify rest of world
+ sendBroadcast(new Intent(ACTION_STARTED));
+
+ while (!shouldExit) {
+ if (acceptWifi) {
+ if (wifiListener != null) {
+ if (!wifiListener.isAlive()) {
+ myLog.l(Log.DEBUG, "Joining crashed wifiListener thread");
+ try {
+ wifiListener.join();
+ } catch (InterruptedException e) {
+ }
+ wifiListener = null;
+ }
+ }
+ if (wifiListener == null) {
+ // Either our wifi listener hasn't been created yet, or has
+ // crashed,
+ // so spawn it
+ wifiListener = new TcpListener(listenSocket, this);
+ wifiListener.start();
+ }
+ }
+ if (acceptNet) {
+ if (proxyConnector != null) {
+ if (!proxyConnector.isAlive()) {
+ myLog.l(Log.DEBUG, "Joining crashed proxy connector");
+ try {
+ proxyConnector.join();
+ } catch (InterruptedException e) {
+ }
+ proxyConnector = null;
+ long nowMillis = new Date().getTime();
+ // myLog.l(Log.DEBUG,
+ // "Now:"+nowMillis+" start:"+proxyStartMillis);
+ if (nowMillis - proxyStartMillis < 3000) {
+ // We assume that if the proxy thread crashed within
+ // 3
+ // seconds of starting, it was a startup or
+ // connection
+ // failure.
+ myLog.l(Log.DEBUG, "Incrementing proxy start failures");
+ consecutiveProxyStartFailures++;
+ } else {
+ // Otherwise assume the proxy started successfully
+ // and
+ // crashed later.
+ myLog.l(Log.DEBUG, "Resetting proxy start failures");
+ consecutiveProxyStartFailures = 0;
+ }
+ }
+ }
+ if (proxyConnector == null) {
+ long nowMillis = new Date().getTime();
+ boolean shouldStartListener = false;
+ // We want to restart the proxy listener without much delay
+ // for the first few attempts, but add a much longer delay
+ // if we consistently fail to connect.
+ if (consecutiveProxyStartFailures < 3
+ && (nowMillis - proxyStartMillis) > 5000) {
+ // Retry every 5 seconds for the first 3 tries
+ shouldStartListener = true;
+ } else if (nowMillis - proxyStartMillis > 30000) {
+ // After the first 3 tries, only retry once per 30 sec
+ shouldStartListener = true;
+ }
+ if (shouldStartListener) {
+ myLog.l(Log.DEBUG, "Spawning ProxyConnector");
+ proxyConnector = new ProxyConnector(this);
+ proxyConnector.start();
+ proxyStartMillis = nowMillis;
+ }
+ }
+ }
+ try {
+ // todo: think about using ServerSocket, and just closing
+ // the main socket to send an exit signal
+ Thread.sleep(WAKE_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ myLog.l(Log.DEBUG, "Thread interrupted");
+ }
+ }
+
+ terminateAllSessions();
+
+ if (proxyConnector != null) {
+ proxyConnector.quit();
+ proxyConnector = null;
+ }
+ if (wifiListener != null) {
+ wifiListener.quit();
+ wifiListener = null;
+ }
+ shouldExit = false; // we handled the exit flag, so reset it to
+ // acknowledge
+ myLog.l(Log.DEBUG, "Exiting cleanly, returning from run()");
+
+ cleanupAndStopService();
+ }
+
+ private void terminateAllSessions() {
+ myLog.i("Terminating " + sessionThreads.size() + " session thread(s)");
+ synchronized (this) {
+ for (SessionThread sessionThread : sessionThreads) {
+ if (sessionThread != null) {
+ sessionThread.closeDataSocket();
+ sessionThread.closeSocket();
+ }
+ }
+ }
+ }
+
+ public void cleanupAndStopService() {
+ // Call the Android Service shutdown function
+ stopSelf();
+ releaseWifiLock();
+ releaseWakeLock();
+ clearNotification();
+ sendBroadcast(new Intent(ACTION_STOPPED));
+ }
+
+ private void takeWakeLock() {
+ if (wakeLock == null) {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+
+ // Many (all?) devices seem to not properly honor a
+ // PARTIAL_WAKE_LOCK,
+ // which should prevent CPU throttling. This has been
+ // well-complained-about on android-developers.
+ // For these devices, we have a config option to force the phone
+ // into a
+ // full wake lock.
+ if (fullWake) {
+ wakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, WAKE_LOCK_TAG);
+ } else {
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
+ }
+ wakeLock.setReferenceCounted(false);
+ }
+ myLog.d("Acquiring wake lock");
+ wakeLock.acquire();
+ }
+
+ private void releaseWakeLock() {
+ myLog.d("Releasing wake lock");
+ if (wakeLock != null) {
+ wakeLock.release();
+ wakeLock = null;
+ myLog.d("Finished releasing wake lock");
+ } else {
+ myLog.i("Couldn't release null wake lock");
+ }
+ }
+
+ // public static void writeMonitor(boolean incoming, String s) {
+ // if(incoming) {
+ // s = "> " + s;
+ // } else {
+ // s = "< " + s;
+ // }
+ // sessionMonitor.add(s.trim());
+ // int maxSize = Defaults.getSessionMonitorScrollBack();
+ // while(sessionMonitor.size() > maxSize) {
+ // sessionMonitor.remove(0);
+ // }
+ // updateClients();
+ // }
+
+ private void takeWifiLock() {
+ myLog.d("Taking wifi lock");
+ if (wifiLock == null) {
+ WifiManager manager = (WifiManager) this.getApplication().getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ wifiLock = manager.createWifiLock("SwiFTP");
+ wifiLock.setReferenceCounted(false);
+ }
+ wifiLock.acquire();
+ }
+
+ private void releaseWifiLock() {
+ myLog.d("Releasing wifi lock");
+ if (wifiLock != null) {
+ wifiLock.release();
+ wifiLock = null;
+ }
+ }
+
+ public void errorShutdown() {
+ myLog.l(Log.ERROR, "Service errorShutdown() called");
+ cleanupAndStopService();
+ }
+
+ /**
+ * The FTPServerService must know about all running session threads so they can be
+ * terminated on exit. Called when a new session is created.
+ */
+ public void registerSessionThread(SessionThread newSession) {
+ // Before adding the new session thread, clean up any finished session
+ // threads that are present in the list.
+
+ // Since we're not allowed to modify the list while iterating over
+ // it, we construct a list in toBeRemoved of threads to remove
+ // later from the sessionThreads list.
+ synchronized (this) {
+ List toBeRemoved = new ArrayList();
+ for (SessionThread sessionThread : sessionThreads) {
+ if (!sessionThread.isAlive()) {
+ myLog.l(Log.DEBUG, "Cleaning up finished session...");
+ try {
+ sessionThread.join();
+ myLog.l(Log.DEBUG, "Thread joined");
+ toBeRemoved.add(sessionThread);
+ sessionThread.closeSocket(); // make sure socket closed
+ } catch (InterruptedException e) {
+ myLog.l(Log.DEBUG, "Interrupted while joining");
+ // We will try again in the next loop iteration
+ }
+ }
+ }
+ for (SessionThread removeThread : toBeRemoved) {
+ sessionThreads.remove(removeThread);
+ }
+
+ // Cleanup is complete. Now actually add the new thread to the list.
+ sessionThreads.add(newSession);
+ }
+ myLog.d("Registered session thread");
+ }
+
+ /** Get the ProxyConnector, may return null if proxying is disabled. */
+ public ProxyConnector getProxyConnector() {
+ return proxyConnector;
+ }
+
+ abstract protected Class> getSettingClass();
+}
diff --git a/qftplib/src/main/java/org/swiftp/Globals.java b/qftplib/src/main/java/org/swiftp/Globals.java
new file mode 100644
index 00000000..8def1ef3
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/Globals.java
@@ -0,0 +1,68 @@
+package org.swiftp;
+
+import java.io.File;
+
+import org.swiftp.server.ProxyConnector;
+
+import android.content.Context;
+
+// TODO: this must all be removed
+// if you need a setting, get it from the settings
+
+public class Globals {
+ private static Context context;
+ private static String lastError;
+ private static File chrootDir = null;
+ private static ProxyConnector proxyConnector = null;
+ private static String username = null;
+
+ public static ProxyConnector getProxyConnector() {
+ if(proxyConnector != null) {
+ if(!proxyConnector.isAlive()) {
+ return null;
+ }
+ }
+ return proxyConnector;
+ }
+
+ public static void setProxyConnector(ProxyConnector proxyConnector) {
+ Globals.proxyConnector = proxyConnector;
+ }
+
+ public static File getChrootDir() {
+ return chrootDir;
+ }
+
+ public static void setChrootDir(File chrootDir) {
+ if(chrootDir.isDirectory()) {
+ Globals.chrootDir = chrootDir;
+ }
+ }
+
+ public static String getLastError() {
+ return lastError;
+ }
+
+ public static void setLastError(String lastError) {
+ Globals.lastError = lastError;
+ }
+
+ public static Context getContext() {
+ return context;
+ }
+
+ public static void setContext(Context context) {
+ if(context != null) {
+ Globals.context = context;
+ }
+ }
+
+ public static String getUsername() {
+ return username;
+ }
+
+ public static void setUsername(String username) {
+ Globals.username = username;
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/MyLog.java b/qftplib/src/main/java/org/swiftp/MyLog.java
new file mode 100644
index 00000000..a2fd284e
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/MyLog.java
@@ -0,0 +1,65 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp;
+
+import android.util.Log;
+
+public class MyLog {
+ protected String tag;
+
+ public MyLog(String tag) {
+ this.tag = tag;
+ }
+
+ public void l(int level, String str, boolean sysOnly) {
+ synchronized (MyLog.class) {
+ str = str.trim();
+ // Messages of this severity are handled specially
+ if(level == Log.ERROR || level == Log.WARN) {
+ Globals.setLastError(str);
+ }
+ if(level >= Defaults.getConsoleLogLevel()) {
+ Log.println(level,tag, str);
+ }
+ if(!sysOnly) { // some messages only go to the Android log
+ if(level >= Defaults.getUiLogLevel()) {
+ FTPServerService.log(level, str);
+ }
+ }
+ }
+ }
+
+ public void l(int level, String str) {
+ l(level, str, false);
+ }
+
+ public void e(String s) {
+ l(Log.ERROR, s, false);
+ }
+ public void w(String s) {
+ l(Log.WARN, s, false);
+ }
+ public void i(String s) {
+ l(Log.INFO, s, false);
+ }
+ public void d(String s) {
+ l(Log.DEBUG, s, false);
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/QuotaStats.java b/qftplib/src/main/java/org/swiftp/QuotaStats.java
new file mode 100644
index 00000000..d86b738c
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/QuotaStats.java
@@ -0,0 +1,37 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp;
+
+public class QuotaStats {
+ private int quota;
+ private int used;
+
+ public QuotaStats(int used, int quota) {
+ this.quota = quota;
+ this.used = used;
+ }
+
+ public int getQuota() {
+ return quota;
+ }
+ public int getUsed() {
+ return used;
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/Settings.java b/qftplib/src/main/java/org/swiftp/Settings.java
new file mode 100644
index 00000000..2ec7aa0e
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/Settings.java
@@ -0,0 +1,83 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp;
+
+import android.util.Log;
+
+public class Settings {
+ protected static int inputBufferSize = 256;
+ protected static boolean allowOverwrite = false;
+ protected static int dataChunkSize = 8192; // do file I/O in 8k chunks
+ protected static int sessionMonitorScrollBack = 10;
+ protected static int serverLogScrollBack = 10;
+ protected static int uiLogLevel = Log.INFO;
+
+ public static int getUiLogLevel() {
+ return uiLogLevel;
+ }
+
+ public static void setUiLogLevel(int uiLogLevel) {
+ Settings.uiLogLevel = uiLogLevel;
+ }
+
+ public static int getInputBufferSize() {
+ return inputBufferSize;
+ }
+
+ public static void setInputBufferSize(int inputBufferSize) {
+ Settings.inputBufferSize = inputBufferSize;
+ }
+
+ public static boolean isAllowOverwrite() {
+ return allowOverwrite;
+ }
+
+ public static void setAllowOverwrite(boolean allowOverwrite) {
+ Settings.allowOverwrite = allowOverwrite;
+ }
+
+ public static int getDataChunkSize() {
+ return dataChunkSize;
+ }
+
+ public static void setDataChunkSize(int dataChunkSize) {
+ Settings.dataChunkSize = dataChunkSize;
+ }
+
+ public static int getSessionMonitorScrollBack() {
+ return sessionMonitorScrollBack;
+ }
+
+ public static void setSessionMonitorScrollBack(
+ int sessionMonitorScrollBack)
+ {
+ Settings.sessionMonitorScrollBack = sessionMonitorScrollBack;
+ }
+
+ public static int getServerLogScrollBack() {
+ return serverLogScrollBack;
+ }
+
+ public static void setLogScrollBack(int serverLogScrollBack) {
+ Settings.serverLogScrollBack = serverLogScrollBack;
+ }
+
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/Util.java b/qftplib/src/main/java/org/swiftp/Util.java
new file mode 100644
index 00000000..70f78d92
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/Util.java
@@ -0,0 +1,198 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.MediaScannerConnection;
+import android.media.MediaScannerConnection.MediaScannerConnectionClient;
+import android.net.Uri;
+import android.provider.Settings;
+import android.util.Log;
+
+abstract public class Util {
+ static MyLog myLog = new MyLog(Util.class.getName());
+ public static String getAndroidId() {
+ ContentResolver cr = Globals.getContext().getContentResolver();
+ return Settings.Secure.getString(cr, Settings.Secure.ANDROID_ID);
+ }
+
+ /**
+ * Get the SwiFTP version from the manifest.
+ * @return The version as a String.
+ */
+ public static String getVersion() {
+ String packageName = Globals.getContext().getPackageName();
+ try {
+ return Globals.getContext().getPackageManager().getPackageInfo(packageName, 0).versionName;
+ } catch ( NameNotFoundException e) {
+ myLog.l(Log.ERROR, "NameNotFoundException looking up SwiFTP version");
+ return null;
+ }
+ }
+
+
+ public static byte byteOfInt(int value, int which) {
+ int shift = which * 8;
+ return (byte)(value >> shift);
+ }
+
+ public static String ipToString(int addr, String sep) {
+ //myLog.l(Log.DEBUG, "IP as int: " + addr);
+ if(addr > 0) {
+ StringBuffer buf = new StringBuffer();
+ buf.
+ append(byteOfInt(addr, 0)).append(sep).
+ append(byteOfInt(addr, 1)).append(sep).
+ append(byteOfInt(addr, 2)).append(sep).
+ append(byteOfInt(addr, 3));
+ myLog.l(Log.DEBUG, "ipToString returning: " + buf.toString());
+ return buf.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public static InetAddress intToInet(int value) {
+ byte[] bytes = new byte[4];
+ for(int i = 0; i<4; i++) {
+ bytes[i] = byteOfInt(value, i);
+ }
+ try {
+ return InetAddress.getByAddress(bytes);
+ } catch (UnknownHostException e) {
+ // This only happens if the byte array has a bad length
+ return null;
+ }
+ }
+
+ public static String ipToString(int addr) {
+ if(addr == 0) {
+ // This can only occur due to an error, we shouldn't blindly
+ // convert 0 to string.
+ myLog.l(Log.INFO, "ipToString won't convert value 0");
+ return null;
+ }
+ return ipToString(addr, ".");
+ }
+
+ // This exists to avoid cluttering up other code with
+ // UnsupportedEncodingExceptions.
+ public static byte[] jsonToByteArray(JSONObject json) throws JSONException {
+ try {
+ return json.toString().getBytes(Defaults.STRING_ENCODING);
+ } catch (UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+
+ // This exists to avoid cluttering up other code with
+ // UnsupportedEncodingExceptions.
+ public static JSONObject byteArrayToJson(byte[] bytes) throws JSONException {
+ try {
+ return new JSONObject(new String(bytes, Defaults.STRING_ENCODING));
+ } catch (UnsupportedEncodingException e) {
+ // This will never happen because we use valid encodings
+ return null;
+ }
+ }
+
+ public static void newFileNotify(String path) {
+ if(Defaults.do_mediascanner_notify) {
+ myLog.l(Log.DEBUG, "Notifying others about new file: " + path);
+ new MediaScannerNotifier(Globals.getContext(), path);
+ }
+ }
+
+ public static void deletedFileNotify(String path) {
+ // This might not work, I couldn't find an API call for this.
+ if(Defaults.do_mediascanner_notify) {
+ myLog.l(Log.DEBUG, "Notifying others about deleted file: " + path);
+ new MediaScannerNotifier(Globals.getContext(), path);
+ }
+ }
+
+ // A class to help notify the Music Player and other media services when
+ // a file has been uploaded. Thanks to Dave Sparks in his post to the
+ // Android Developers mailing list on 14 Feb 2009.
+ private static class MediaScannerNotifier implements MediaScannerConnectionClient {
+ private final MediaScannerConnection connection;
+ private final String path;
+
+ public MediaScannerNotifier(Context context, String path) {
+ this.path = path;
+ connection = new MediaScannerConnection(context, this);
+ connection.connect();
+ }
+
+ public void onMediaScannerConnected() {
+ connection.scanFile(path, null); // null: we don't know MIME type
+ }
+
+ public void onScanCompleted(String path, Uri uri) {
+ connection.disconnect();
+ }
+ }
+
+ public static String[] concatStrArrays(String[] a1, String[] a2) {
+ String[] retArr = new String[a1.length + a2.length];
+ System.arraycopy(a1, 0, retArr, 0, a1.length);
+ System.arraycopy(a2, 0, retArr, a1.length, a2.length);
+ return retArr;
+ }
+
+ public static void sleepIgnoreInterupt(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch(InterruptedException e) {}
+ }
+
+ public static String getCode(Context context) {
+ String packageName = context.getPackageName();
+ String[] xcode = packageName.split("\\.");
+ String code = xcode[xcode.length-1];
+ return code;
+ }
+
+ public static String getSP(Context context, String key) {
+ String val;
+ SharedPreferences obj = context.getSharedPreferences("passinger_db",0);
+ val = obj.getString(key,"");
+ return val;
+ }
+ public static void setSP(Context context, String key,String val) {
+ SharedPreferences obj = context.getSharedPreferences("passinger_db",0);
+ Editor wobj;
+ wobj = obj.edit();
+ wobj.putString(key, val);
+ wobj.commit();
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/WidgetProvider.java b/qftplib/src/main/java/org/swiftp/WidgetProvider.java
new file mode 100644
index 00000000..ae661a53
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/WidgetProvider.java
@@ -0,0 +1,121 @@
+package org.swiftp;
+
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.view.View;
+import android.widget.RemoteViews;
+
+/**
+ * Class handles Widget Events.
+ *
+ */
+@TargetApi(3)
+public class WidgetProvider extends AppWidgetProvider {
+
+ public static String ACTION_WIDGET_BUTTON = "actionWidgetButton";
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager,
+ int[] appWidgetIds) {
+
+ // Log.d("MyActivity", "onUpdate");
+ // Toast.makeText(context, "onUpdate", Toast.LENGTH_SHORT).show();
+
+ // register new Widgets, for them to be Updated by WidgetUiUpdater
+ WidgetUiUpdater.registerWidgets(appWidgetIds, context, appWidgetManager);
+
+ // add ButtonListener
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
+ R.layout.ftp_widget);
+
+ Intent active = new Intent(context, WidgetProvider.class);
+ active.setAction(ACTION_WIDGET_BUTTON);
+
+ PendingIntent actionPendingIntent = PendingIntent.getBroadcast(context, 0,
+ active, PendingIntent.FLAG_IMMUTABLE);
+ remoteViews.setOnClickPendingIntent(R.id.widget_button_off, actionPendingIntent);
+ remoteViews.setOnClickPendingIntent(R.id.widget_button_on, actionPendingIntent);
+
+ // set the right state, according to the FTP Server
+ if (FTPServerService.isRunning()) {
+ remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE);
+ } else {
+ remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE);
+ }
+ appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
+
+ super.onUpdate(context, appWidgetManager, appWidgetIds);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ // Log.d("MyActivity", "onReceive");
+ // Toast.makeText(context, "onReceive, Action: "+ intent.getAction(),
+ // Toast.LENGTH_SHORT).show();
+
+ if (intent.getAction().equals(ACTION_WIDGET_BUTTON)) {
+
+ /*
+ * After handling onReceive this Object will be destroyed by the OS.
+ * BroadcastReceivers and WidgetProviders shouldn't handle asynchronous
+ * actions, like UI Updates.
+ */
+
+ // start or stop FTP Service
+ Intent intentFTP = new Intent(context, FTPServerService.class);
+
+ if (!FTPServerService.isRunning()) {
+ context.startService(intentFTP);
+ } else {
+ context.stopService(intentFTP);
+ }
+ // TODO: Use intent to update UI
+ // UiUpdater.updateClients();
+ }
+ super.onReceive(context, intent);
+ }
+
+ @Override
+ public void onDeleted(Context context, int[] appWidgetIds) {
+
+ // Log.d("MyActivity", "onDeleted");
+ // Toast.makeText(context, "onDeleted", Toast.LENGTH_SHORT).show();
+
+ WidgetUiUpdater.unregisterWidgets(appWidgetIds);
+ super.onDeleted(context, appWidgetIds);
+ }
+
+ @Override
+ public void onEnabled(Context context) {
+
+ // Log.d("MyActivity", "onEnabled");
+ // Toast.makeText(context, "onEnabled", Toast.LENGTH_SHORT).show();
+
+ // register the WidgetUiUpdater for the UiUpdater Messages
+ WidgetUiUpdater.registerAtUiUpdater();
+ super.onEnabled(context);
+ }
+
+ @Override
+ public void onDisabled(Context context) {
+
+ // Log.d("MyActivity", "onDisabled");
+ // Toast.makeText(context, "onDisabled", Toast.LENGTH_SHORT).show();
+
+ // unregister the WidgetUiUpdater from the UiUpdater Messages
+ WidgetUiUpdater.unregisterAtUiUpdater();
+
+ // and clean the WidgetIds list, if it is still not empty
+ WidgetUiUpdater.unregisterAllWidgets();
+ super.onDisabled(context);
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/WidgetUiUpdater.java b/qftplib/src/main/java/org/swiftp/WidgetUiUpdater.java
new file mode 100644
index 00000000..63f1a4c0
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/WidgetUiUpdater.java
@@ -0,0 +1,124 @@
+package org.swiftp;
+
+import java.util.HashSet;
+
+import android.annotation.TargetApi;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.widget.RemoteViews;
+
+/**
+ * This class maintains Widget Updates. It obtains information about UI Updates from
+ * UiUpdater.class. For starting getting UiUpdates, method {@link #registerAtUiUpdater()}
+ * have to be executed once.
+ *
+ * Why does this Class exist? This class exists, because I was not able to store
+ * {@link #handler} for every single Widget. There was a Problem with unregistering
+ * handlers, when a Widget was deleted.
+ */
+public class WidgetUiUpdater {
+ private static HashSet widgetIds = new HashSet();
+ private static Context context;
+ private static AppWidgetManager appWidgetManager;
+
+ @SuppressWarnings("unused")
+ private static Handler handler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case 0: // We are being told to do a UI update
+ // If more than one UI update is queued up, we only need to do one.
+ removeMessages(0);
+ updateWidgetUi();
+ break;
+ case 1: // We are being told to display an error message
+ removeMessages(1);
+ }
+ }
+ };
+
+ /**
+ * Registers new Widgets, for updating them when needed. UiUpdater sends UI update
+ * messages.
+ *
+ * @param newWidgetIds
+ * @param newContext
+ * @param newAppWidgetManager
+ */
+ @TargetApi(3)
+ protected static void registerWidgets(int[] newWidgetIds, Context newContext,
+ AppWidgetManager newAppWidgetManager) {
+ context = newContext;
+ appWidgetManager = newAppWidgetManager;
+
+ for (int newWidgetId : newWidgetIds) {
+ widgetIds.add(new Integer(newWidgetId));
+ }
+ }
+
+ /**
+ * Unregisters deleted Widgets.
+ *
+ * @param newWidgetIds
+ */
+ protected static void unregisterWidgets(int[] newWidgetIds) {
+
+ if (!widgetIds.isEmpty()) {
+ for (int newWidgetId : newWidgetIds) {
+ widgetIds.remove(new Integer(newWidgetId));
+ }
+ }
+
+ }
+
+ /**
+ * Unregister all Widgets on the List.
+ */
+ protected static void unregisterAllWidgets() {
+ widgetIds.clear();
+ }
+
+ /**
+ * Start listening for UI Updates, to know, when updating the Widgets
+ */
+ protected static void registerAtUiUpdater() {
+ // TODO: fix this part of the code
+ // UiUpdater.registerClient(handler);
+ }
+
+ /**
+ * Stop listening for UI Updates.
+ */
+ protected static void unregisterAtUiUpdater() {
+ // TODO: fix this part of the code
+ // UiUpdater.unregisterClient(handler);
+ }
+
+ /**
+ * Updates all Widgets, when static UiUpdater (Observer Pattern) tells to.
+ */
+ private static void updateWidgetUi() {
+
+ // tell all Widgets, to Update themselves. Right State is set in the onUpdate
+ // handler.
+ RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
+ R.layout.ftp_widget);
+
+ // set the right state, according to the FTP Server
+ if (FTPServerService.isRunning()) {
+ remoteViews.setViewVisibility(R.id.widget_button_on, View.VISIBLE);
+ remoteViews.setViewVisibility(R.id.widget_button_off, View.GONE);
+ } else {
+ remoteViews.setViewVisibility(R.id.widget_button_on, View.GONE);
+ remoteViews.setViewVisibility(R.id.widget_button_off, View.VISIBLE);
+ }
+
+ for (Integer widgetId : widgetIds) {
+ appWidgetManager.updateAppWidget(widgetId.intValue(), remoteViews);
+ }
+
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java b/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java
new file mode 100644
index 00000000..5a4fa8d6
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceActivity.java
@@ -0,0 +1,56 @@
+package org.swiftp.gui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+
+import org.swiftp.R;
+
+/**
+ * This is the main activity for swiftp, it enables the user to start the server service
+ * and allows the users to change the settings.
+ */
+public class ServerPreferenceActivity extends PreferenceActivity{
+
+ private static String TAG = ServerPreferenceActivity.class.getSimpleName();
+
+
+
+ public static void start(Context context) {
+ Intent starter = new Intent(context, ServerPreferenceActivity.class);
+ context.startActivity(starter);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.ftp_preferences);
+ setContentView(R.layout.activity_preference);
+
+
+ }
+
+
+
+ @Override
+ protected void onResume() {
+ Log.v(TAG, "onResume");
+ super.onResume();
+
+ }
+
+ @Override
+ protected void onPause() {
+ Log.v(TAG, "onPause");
+ super.onPause();
+
+ Log.v(TAG, "Unregistering the FTPServer actions");
+
+
+ }
+
+
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java b/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java
new file mode 100644
index 00000000..432306c1
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/gui/ServerPreferenceFragment.java
@@ -0,0 +1,308 @@
+package org.swiftp.gui;
+
+import android.annotation.TargetApi;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.widget.Toast;
+
+import org.swiftp.Defaults;
+import org.swiftp.FTPServerService;
+import org.swiftp.Globals;
+import org.swiftp.R;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+/**
+ * Created by Hmei on 2017-06-07.
+ */
+
+@TargetApi(Build.VERSION_CODES.HONEYCOMB)
+public class ServerPreferenceFragment extends PreferenceFragment implements
+ SharedPreferences.OnSharedPreferenceChangeListener {
+ private static final String TAG = "SPF";
+ EditTextPreference mPassWordPref;
+ /**
+ * This receiver will check FTPServer.ACTION* messages and will update the button,
+ * running_state, if the server is running and will also display at what url the
+ * server is running.
+ */
+ BroadcastReceiver ftpServerReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.v(TAG, "FTPServerService action received: " + intent.getAction());
+ CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state");
+ if (intent.getAction().equals(FTPServerService.ACTION_STARTED)) {
+ running_state.setChecked(true);
+ // Fill in the FTP server address
+ String[] address = FTPServerService.getIpPortString();
+ if (address == null) {
+ Log.v(TAG, "Unable to retreive wifi ip address");
+ running_state.setSummary(R.string.cant_get_url);
+ return;
+ }
+ StringBuilder iptext = new StringBuilder();
+ for(String ip : address){
+ if(iptext.length()>0)
+ iptext.append(", ");
+ iptext.append(ip);
+ }
+ Resources resources = getResources();
+ String summary = resources.getString(R.string.running_summary_started,
+ iptext);
+ running_state.setSummary(summary);
+ } else if (intent.getAction().equals(FTPServerService.ACTION_STOPPED)) {
+ running_state.setChecked(false);
+ running_state.setSummary(R.string.running_summary_stopped);
+ } else if (intent.getAction().equals(FTPServerService.ACTION_FAILEDTOSTART)) {
+ running_state.setChecked(false);
+ running_state.setSummary(R.string.running_summary_failed);
+ }
+ }
+ };
+
+ static private String transformPassword(String password) {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(Globals
+ .getContext());
+ Resources res = Globals.getContext().getResources();
+ boolean showPassword = res.getString(R.string.show_password_default).equals("true");
+ showPassword = sp.getBoolean("show_password", showPassword);
+ if (showPassword == true)
+ return password;
+ else {
+ StringBuilder sb = new StringBuilder(password.length());
+ for (int i = 0; i < password.length(); ++i)
+ sb.append('*');
+ return sb.toString();
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.ftp_preferences);
+
+ final SharedPreferences settings = PreferenceManager
+ .getDefaultSharedPreferences(getActivity());
+ Resources resources = getResources();
+
+ CheckBoxPreference running_state = (CheckBoxPreference) findPreference("running_state");
+ running_state.setChecked(FTPServerService.isRunning());
+ running_state.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if ((Boolean) newValue) {
+ startServer();
+ } else {
+ stopServer();
+ }
+ return true;
+ }
+ });
+
+ EditTextPreference username_pref = (EditTextPreference) findPreference("username");
+ username_pref.setSummary(settings.getString("username",
+ resources.getString(R.string.username_default)));
+ username_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String newUsername = (String) newValue;
+ if (preference.getSummary().equals(newUsername))
+ return false;
+ if (!newUsername.matches("[a-zA-Z0-9]+")) {
+ Toast.makeText(getActivity(),
+ R.string.username_validation_error, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ preference.setSummary(newUsername);
+ stopServer();
+ return true;
+ }
+ });
+
+ mPassWordPref = (EditTextPreference) findPreference("password");
+ String password = resources.getString(R.string.password_default);
+ password = settings.getString("password", password);
+ mPassWordPref.setSummary(transformPassword(password));
+ mPassWordPref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String newPassword = (String) newValue;
+ if (!newPassword.matches("[a-zA-Z0-9]+")) {
+ Toast.makeText(getActivity(),
+ R.string.password_validation_error, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ preference.setSummary(transformPassword(newPassword));
+ stopServer();
+ return true;
+ }
+ });
+
+ EditTextPreference portnum_pref = (EditTextPreference) findPreference("portNum");
+ portnum_pref.setSummary(settings.getString("portNum",
+ getString(R.string.portnumber_default)));
+ portnum_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String newPortnumString = (String) newValue;
+ if (preference.getSummary().equals(newPortnumString))
+ return false;
+ int portnum = 0;
+ try {
+ portnum = Integer.parseInt(newPortnumString);
+ } catch (Exception e) {
+ }
+ if (portnum <= 0 || 65535 < portnum) {
+ Toast.makeText(getActivity(),
+ R.string.port_validation_error, Toast.LENGTH_LONG).show();
+ return false;
+ }
+ preference.setSummary(newPortnumString);
+ stopServer();
+ return true;
+ }
+ });
+
+ EditTextPreference chroot_pref = (EditTextPreference) findPreference("chrootDir");
+ chroot_pref.setSummary(settings.getString("chrootDir",
+ resources.getString(R.string.chroot_default)));
+ chroot_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String newChroot = (String) newValue;
+ if (preference.getSummary().equals(newChroot))
+ return false;
+ // now test the new chroot directory
+ File chrootTest = new File(newChroot);
+ if (!chrootTest.isDirectory() || !chrootTest.canRead())
+ return false;
+ preference.setSummary(newChroot);
+ stopServer();
+ return true;
+ }
+ });
+
+ final CheckBoxPreference wakelock_pref = (CheckBoxPreference) findPreference("stayAwake");
+ wakelock_pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ stopServer();
+ return true;
+ }
+ });
+
+ Preference help = findPreference("help");
+ help.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.help_dlg_title)
+ .setMessage(R.string.help_dlg_message)
+ .setPositiveButton(getText(R.string.ok), null).show();
+ return true;
+ }
+ });
+
+ Preference about = findPreference("about");
+ about.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.about_dlg_title)
+ .setMessage(R.string.about_dlg_message)
+ .setPositiveButton(getText(R.string.ok), null).show();
+ return true;
+ }
+ });
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
+ if (key.equals("show_password")) {
+ Resources res = Globals.getContext().getResources();
+ String password = res.getString(R.string.password_default);
+ password = sp.getString("password", password);
+ mPassWordPref.setSummary(transformPassword(password));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ Log.v(TAG, "onResume");
+ super.onResume();
+
+ // make this class listen for preference changes
+ getPreferenceScreen().getSharedPreferences()
+ .registerOnSharedPreferenceChangeListener(this);
+
+ Log.v(TAG, "Registering the FTP server actions");
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(FTPServerService.ACTION_STARTED);
+ filter.addAction(FTPServerService.ACTION_STOPPED);
+ filter.addAction(FTPServerService.ACTION_FAILEDTOSTART);
+ getActivity().registerReceiver(ftpServerReceiver, filter);
+ }
+
+ @Override
+ public void onPause() {
+ Log.v(TAG, "onPause");
+ super.onPause();
+
+ Log.v(TAG, "Unregistering the FTPServer actions");
+ getActivity().unregisterReceiver(ftpServerReceiver);
+
+ // unregister the listener
+ getPreferenceScreen().getSharedPreferences()
+ .unregisterOnSharedPreferenceChangeListener(this);
+
+ }
+
+ private void startServer() {
+ Context context = getActivity().getApplicationContext();
+ Intent serverService = new Intent(context, FTPServerService.class);
+ if (!FTPServerService.isRunning()) {
+ warnIfNoExternalStorage();
+ getActivity().startService(serverService);
+ }
+ }
+
+ private void stopServer() {
+ Context context = getActivity().getApplicationContext();
+ Intent serverService = new Intent(context, FTPServerService.class);
+ getActivity().stopService(serverService);
+ }
+
+ /**
+ * Will check if the device contains external storage (sdcard) and display a warning
+ * for the user if there is no external storage. Nothing more.
+ */
+ private void warnIfNoExternalStorage() {
+ String storageState = Environment.getExternalStorageState();
+ if (!storageState.equals(Environment.MEDIA_MOUNTED)) {
+ Log.v(TAG, "Warning due to storage stat" + storageState);
+ Toast toast = Toast.makeText(getActivity(), R.string.storage_warning,
+ Toast.LENGTH_LONG);
+ toast.setGravity(Gravity.CENTER, 0, 0);
+ toast.show();
+ }
+ }
+
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/Account.java b/qftplib/src/main/java/org/swiftp/server/Account.java
new file mode 100644
index 00000000..28e43374
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/Account.java
@@ -0,0 +1,34 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+public class Account {
+ protected String username = null;
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdAPPE.java b/qftplib/src/main/java/org/swiftp/server/CmdAPPE.java
new file mode 100644
index 00000000..045f4046
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdAPPE.java
@@ -0,0 +1,35 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+
+public class CmdAPPE extends CmdAbstractStore implements Runnable {
+ protected String input;
+
+ public CmdAPPE(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdAPPE.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ doStorOrAppe(getParameter(input), true);
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdAbstractListing.java b/qftplib/src/main/java/org/swiftp/server/CmdAbstractListing.java
new file mode 100644
index 00000000..4ff62511
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdAbstractListing.java
@@ -0,0 +1,94 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+/*
+ * Since the FTP verbs LIST and NLST do very similar things related to listing
+ * directory contents, the common tasks that they share have been factored
+ * out into this abstract class. Both CmdLIST and CmdNLST inherit from this
+ * class.
+ */
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import org.swiftp.MyLog;
+
+import android.util.Log;
+
+public abstract class CmdAbstractListing extends FtpCmd {
+ protected static MyLog staticLog = new MyLog(CmdLIST.class.toString());
+
+ public CmdAbstractListing(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdAbstractListing.class.toString());
+ }
+
+ abstract String makeLsString(File file);
+
+ // Creates a directory listing by finding the contents of the directory,
+ // calling makeLsString on each file, and concatenating the results.
+ // Returns an error string if failure, returns null on success. May be
+ // called by CmdLIST or CmdNLST, since they each override makeLsString
+ // in a different way.
+ public String listDirectory(StringBuilder response, File dir) {
+ if(!dir.isDirectory()) {
+ return "500 Internal error, listDirectory on non-directory\r\n";
+ }
+ myLog.l(Log.DEBUG, "Listing directory: " + dir.toString());
+
+ // Get a listing of all files and directories in the path
+ File[] entries = dir.listFiles();
+ if(entries == null) {
+ return "500 Couldn't list directory. Check config and mount status.\r\n";
+ }
+ myLog.l(Log.DEBUG, "Dir len " + entries.length);
+ for(File entry : entries) {
+ String curLine = makeLsString(entry);
+ if(curLine != null) {
+ response.append(curLine);
+ }
+ }
+ return null;
+ }
+
+ // Send the directory listing over the data socket. Used by CmdLIST and
+ // CmdNLST.
+ // Returns an error string on failure, or returns null if successful.
+ protected String sendListing(String listing) {
+ if(sessionThread.startUsingDataSocket()) {
+ myLog.l(Log.DEBUG, "LIST/NLST done making socket");
+ } else {
+ sessionThread.closeDataSocket();
+ return "425 Error opening data socket\r\n";
+ }
+ String mode = sessionThread.isBinaryMode() ? "BINARY" : "ASCII";
+ sessionThread.writeString(
+ "150 Opening "+mode+" mode data connection for file list\r\n");
+ myLog.l(Log.DEBUG, "Sent code 150, sending listing string now");
+ if(!sessionThread.sendViaDataSocket(listing)) {
+ myLog.l(Log.DEBUG, "sendViaDataSocket failure");
+ sessionThread.closeDataSocket();
+ return "426 Data socket or network error\r\n";
+ }
+ sessionThread.closeDataSocket();
+ myLog.l(Log.DEBUG, "Listing sendViaDataSocket success");
+ sessionThread.writeString("226 Data transmission OK\r\n");
+ return null;
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdAbstractStore.java b/qftplib/src/main/java/org/swiftp/server/CmdAbstractStore.java
new file mode 100644
index 00000000..cc7465a6
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdAbstractStore.java
@@ -0,0 +1,217 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+/**
+ * Since STOR and APPE are essentially identical except for append vs truncate,
+ * the common code is in this class, and inherited by CmdSTOR and CmdAPPE.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import org.swiftp.Defaults;
+import org.swiftp.Globals;
+import org.swiftp.Util;
+
+import android.util.Log;
+
+import util.DocumentUtil;
+import util.FileUtil;
+
+
+abstract public class CmdAbstractStore extends FtpCmd {
+ public static final String message = "TEMPLATE!!";
+
+ public CmdAbstractStore(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdAbstractStore.class.toString());
+ }
+
+ public void doStorOrAppe(String param, boolean append) {
+ myLog.l(Log.DEBUG, "STOR/APPE executing with append=" + append);
+ File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+
+ String errString = null;
+ FileOutputStream out = null;
+ //DedicatedWriter dedicatedWriter = null;
+// int origPriority = Thread.currentThread().getPriority();
+// myLog.l(Log.DEBUG, "STOR original priority: " + origPriority);
+ storing: {
+ // Get a normalized absolute path for the desired file
+ if(violatesChroot(storeFile)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break storing;
+ }
+ if(storeFile.isDirectory()) {
+ errString = "451 Can't overwrite a directory\r\n";
+ break storing;
+ }
+
+ try {
+ if(storeFile.exists()) {
+ if(!append) {
+ if(!storeFile.delete()) {
+ errString = "451 Couldn't truncate file\r\n";
+ break storing;
+ }
+ // Notify other apps that we just deleted a file
+ Util.deletedFileNotify(storeFile.getPath());
+ }
+ }
+ out = FileUtil.getFileOutputStream(storeFile,append);
+ } catch(Exception e) {
+ try {
+ errString = "451 Couldn't open file \"" + param + "\" aka \"" +
+ storeFile.getCanonicalPath() + "\" for writing\r\n";
+ } catch (IOException io_e) {
+ errString = "451 Couldn't open file, nested exception\r\n";
+ }
+ break storing;
+ }
+ if(!sessionThread.startUsingDataSocket()) {
+ errString = "425 Couldn't open data socket\r\n";
+ break storing;
+ }
+ myLog.l(Log.DEBUG, "Data socket ready");
+ sessionThread.writeString("150 Data socket ready\r\n");
+ byte[] buffer = new byte[Defaults.getDataChunkSize()];
+ //dedicatedWriter = new DedicatedWriter(out);
+ //dedicatedWriter.start(); // start the writer thread executing
+ //myLog.l(Log.DEBUG, "Started DedicatedWriter");
+ int numRead;
+// Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
+// int newPriority = Thread.currentThread().getPriority();
+// myLog.l(Log.DEBUG, "New STOR prio: " + newPriority);
+ if(sessionThread.isBinaryMode() ) {
+ myLog.d("Mode is binary");
+ } else {
+ myLog.d("Mode is ascii");
+ }
+ while(true) {
+ /*if(dedicatedWriter.checkErrorFlag()) {
+ errString = "451 File IO problem\r\n";
+ break storing;
+ }*/
+ switch(numRead = sessionThread.receiveFromDataSocket(buffer)) {
+ case -1:
+ myLog.l(Log.DEBUG, "Returned from final read");
+ // We're finished reading
+ break storing;
+ case 0:
+ errString = "426 Couldn't receive data\r\n";
+ break storing;
+ case -2:
+ errString = "425 Could not connect data socket\r\n";
+ break storing;
+ default:
+// myLog.d("Read " + numRead + " bytes from socket");
+ try {
+ //myLog.l(Log.DEBUG, "Enqueueing buffer of " + numRead);
+ //dedicatedWriter.enqueueBuffer(buffer, numRead);
+ if(sessionThread.isBinaryMode()) {
+ out.write(buffer, 0, numRead);
+ } else {
+ // ASCII mode, substitute \r\n to \n
+ int startPos=0, endPos;
+ for(endPos = 0; endPos < numRead; endPos++ ) {
+ if(buffer[endPos] == '\r') {
+ // Our hacky method is to drop all \r
+ out.write(buffer, startPos, endPos-startPos);
+ startPos = endPos+1;
+ }
+ }
+ // Write last part of buffer as long as there was something
+ // left after handling the last \r
+ if(startPos < numRead) {
+ out.write(buffer, startPos, endPos-startPos);
+ }
+ }
+
+ // Attempted bugfix for transfer stalls. Reopen file periodically.
+ //bytesSinceReopen += numRead;
+ //if(bytesSinceReopen >= Defaults.bytes_between_reopen &&
+ // Defaults.do_reopen_hack) {
+ // myLog.d("Closing and reopening file: " + storeFile);
+ // out.close();
+ // out = new FileOutputStream(storeFile, true/*append*/);
+ // bytesSinceReopen = 0;
+ //}
+
+ // Attempted bugfix for transfer stalls. Flush file periodically.
+ //bytesSinceFlush += numRead;
+ //if(bytesSinceFlush >= Defaults.bytes_between_flush &&
+ // Defaults.do_flush_hack) {
+ // myLog.d("Flushing: " + storeFile);
+ // out.flush();
+ // bytesSinceFlush = 0;
+ //}
+
+ // If this transfer fails, a later APPEND operation might be
+ // received. In that case, we will need to have flushed the
+ // previous writes in order for the append to work. The
+ // filesystem on my G1 doesn't seem to recognized unflushed
+ // data when appending.
+ out.flush();
+
+ } catch (IOException e) {
+ errString = "451 File IO problem. Device might be full.\r\n";
+ myLog.d("Exception while storing: " + e);
+ myLog.d("Message: " + e.getMessage());
+ myLog.d("Stack trace: ");
+ StackTraceElement[] traceElems = e.getStackTrace();
+ for(StackTraceElement elem : traceElems) {
+ myLog.d(elem.toString());
+ }
+ break storing;
+ }
+ break;
+ }
+ }
+ }
+// // Clean up the dedicated writer thread
+// if(dedicatedWriter != null) {
+// dedicatedWriter.exit(); // set its exit flag
+// dedicatedWriter.interrupt(); // make sure it wakes up to process the flag
+// }
+// Thread.currentThread().setPriority(origPriority);
+ try {
+// if(dedicatedWriter != null) {
+// dedicatedWriter.exit();
+// }
+ if(out != null) {
+ out.close();
+ }
+ } catch (IOException e) {}
+
+ if(errString != null) {
+ myLog.l(Log.INFO, "STOR error: " + errString.trim());
+ sessionThread.writeString(errString);
+ } else {
+ sessionThread.writeString("226 Transmission complete\r\n");
+ // Notify the music player (and possibly others) that a few file has
+ // been uploaded.
+ Util.newFileNotify(storeFile.getPath());
+ }
+ sessionThread.closeDataSocket();
+ myLog.l(Log.DEBUG, "STOR finished");
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdCDUP.java b/qftplib/src/main/java/org/swiftp/server/CmdCDUP.java
new file mode 100644
index 00000000..eb1602c6
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdCDUP.java
@@ -0,0 +1,76 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.util.Log;
+
+public class CmdCDUP extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdCDUP(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdCDUP.class.toString());
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "CDUP executing");
+ File newDir;
+ String errString = null;
+ mainBlock: {
+ File workingDir = sessionThread.getWorkingDir();
+ newDir = workingDir.getParentFile();
+ if(newDir == null) {
+ errString = "550 Current dir cannot find parent\r\n";
+ break mainBlock;
+ }
+ // Ensure the new path does not violate the chroot restriction
+ if(violatesChroot(newDir)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainBlock;
+ }
+
+ try {
+ newDir = newDir.getCanonicalFile();
+ if(!newDir.isDirectory()) {
+ errString = "550 Can't CWD to invalid directory\r\n";
+ break mainBlock;
+ } else if(newDir.canRead()) {
+ sessionThread.setWorkingDir(newDir);
+ } else {
+ errString = "550 That path is inaccessible\r\n";
+ break mainBlock;
+ }
+ } catch(IOException e) {
+ errString = "550 Invalid path\r\n";
+ break mainBlock;
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.i("CDUP error: " + errString);
+ } else {
+ sessionThread.writeString("200 CDUP successful\r\n");
+ myLog.l(Log.DEBUG, "CDUP success");
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdCWD.java b/qftplib/src/main/java/org/swiftp/server/CmdCWD.java
new file mode 100644
index 00000000..2b577793
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdCWD.java
@@ -0,0 +1,69 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.util.Log;
+
+public class CmdCWD extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdCWD(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdCWD.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "CWD executing");
+ String param = getParameter(input);
+ File newDir;
+ String errString = null;
+ mainblock: {
+ newDir = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+
+ // Ensure the new path does not violate the chroot restriction
+ if(violatesChroot(newDir)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, errString);
+ break mainblock;
+ }
+
+ try {
+ newDir = newDir.getCanonicalFile();
+ if(!newDir.isDirectory()) {
+ sessionThread.writeString("550 Can't CWD to invalid directory\r\n");
+ } else if(newDir.canRead()) {
+ sessionThread.setWorkingDir(newDir);
+ sessionThread.writeString("250 CWD successful\r\n");
+ } else {
+ sessionThread.writeString("550 That path is inaccessible\r\n");
+ }
+ } catch(IOException e) {
+ sessionThread.writeString("550 Invalid path\r\n");
+ break mainblock;
+ }
+ }
+ myLog.l(Log.DEBUG, "CWD complete");
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdDELE.java b/qftplib/src/main/java/org/swiftp/server/CmdDELE.java
new file mode 100644
index 00000000..e17d7983
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdDELE.java
@@ -0,0 +1,64 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import org.swiftp.Globals;
+import org.swiftp.Util;
+
+import android.util.Log;
+
+import util.DocumentUtil;
+import util.FileUtil;
+
+public class CmdDELE extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdDELE(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdDELE.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.INFO, "DELE executing");
+ String param = getParameter(input);
+ File storeFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ String errString = null;
+ if(violatesChroot(storeFile)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ } else if(storeFile.isDirectory()) {
+ errString = "550 Can't DELE a directory\r\n";
+ } else if(!FileUtil.delete(storeFile)) {
+ errString = "450 Error deleting file\r\n";
+ }
+
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, "DELE failed: " + errString.trim());
+ } else {
+ sessionThread.writeString("250 File successfully deleted\r\n");
+ Util.deletedFileNotify(storeFile.getPath());
+ }
+ myLog.l(Log.INFO, "DELE finished");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdFEAT.java b/qftplib/src/main/java/org/swiftp/server/CmdFEAT.java
new file mode 100644
index 00000000..f3943f55
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdFEAT.java
@@ -0,0 +1,40 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdFEAT extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+
+ public CmdFEAT(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdFEAT.class.toString());
+ }
+
+ @Override
+ public void run() {
+ //sessionThread.writeString("211 No extended features\r\n");
+ sessionThread.writeString("211-Features supported\r\n");
+ sessionThread.writeString(" UTF8\r\n"); // advertise UTF8 support (fixes bug 14)
+ sessionThread.writeString("211 End\r\n");
+ myLog.l(Log.DEBUG, "Gave FEAT response");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdLIST.java b/qftplib/src/main/java/org/swiftp/server/CmdLIST.java
new file mode 100644
index 00000000..6729f0c1
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdLIST.java
@@ -0,0 +1,165 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+/* The code that is common to LIST and NLST is implemented in the abstract
+ * class CmdAbstractListing, which is inherited here.
+ * CmdLIST and CmdNLST just override the
+ * makeLsString() function in different ways to provide the different forms
+ * of output.
+ */
+
+package org.swiftp.server;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.util.Log;
+
+public class CmdLIST extends CmdAbstractListing implements Runnable {
+ // The approximate number of milliseconds in 6 months
+ public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000;
+ private final String input;
+
+ public CmdLIST(SessionThread sessionThread, String input) {
+ super(sessionThread, input);
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String errString = null;
+
+ mainblock: {
+ String param = getParameter(input);
+ myLog.d("LIST parameter: " + param);
+ while(param.startsWith("-")) {
+ // Skip all dashed -args, if present
+ myLog.d("LIST is skipping dashed arg " + param);
+ param = getParameter(param);
+ }
+ File fileToList = null;
+ if(param.equals("")) {
+ fileToList = sessionThread.getWorkingDir();
+ } else {
+ if(param.contains("*")) {
+ errString = "550 LIST does not support wildcards\r\n";
+ break mainblock;
+ }
+ fileToList = new File(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(fileToList)) {
+ errString = "450 Listing target violates chroot\r\n";
+ break mainblock;
+ }
+ }
+ String listing;
+ if(fileToList.isDirectory()) {
+ StringBuilder response = new StringBuilder();
+ errString = listDirectory(response, fileToList);
+ if(errString != null) {
+ break mainblock;
+ }
+ listing = response.toString();
+ } else {
+ listing = makeLsString(fileToList);
+ if(listing == null) {
+ errString = "450 Couldn't list that file\r\n";
+ break mainblock;
+ }
+ }
+ errString = sendListing(listing);
+ if(errString != null) {
+ break mainblock;
+ }
+ }
+
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.DEBUG, "LIST failed with: " + errString);
+ } else {
+ myLog.l(Log.DEBUG, "LIST completed OK");
+ }
+ // The success or error response over the control connection will
+ // have already been handled by sendListing, so we can just quit now.
+ }
+
+ // Generates a line of a directory listing in the traditional /bin/ls
+ // format.
+ @Override
+ protected String makeLsString(File file) {
+ StringBuilder response = new StringBuilder();
+
+ if(!file.exists()) {
+ staticLog.l(Log.INFO, "makeLsString had nonexistent file");
+ return null;
+ }
+
+ // See Daniel Bernstein's explanation of /bin/ls format at:
+ // http://cr.yp.to/ftp/list/binls.html
+ // This stuff is almost entirely based on his recommendations.
+
+ String lastNamePart = file.getName();
+ // Many clients can't handle files containing these symbols
+ if(lastNamePart.contains("*") ||
+ lastNamePart.contains("/"))
+ {
+ staticLog.l(Log.INFO, "Filename omitted due to disallowed character");
+ return null;
+ } else {
+ // The following line generates many calls in large directories
+ //staticLog.l(Log.DEBUG, "Filename: " + lastNamePart);
+ }
+
+
+ if(file.isDirectory()) {
+ response.append("drwxr-xr-x 1 owner group");
+ } else {
+ // todo: think about special files, symlinks, devices
+ response.append("-rw-r--r-- 1 owner group");
+ }
+
+ // The next field is a 13-byte right-justified space-padded file size
+ long fileSize = file.length();
+ String sizeString = new Long(fileSize).toString();
+ int padSpaces = 13 - sizeString.length();
+ while(padSpaces-- > 0) {
+ response.append(' ');
+ }
+ response.append(sizeString);
+
+ // The format of the timestamp varies depending on whether the mtime
+ // is 6 months old
+ long mTime = file.lastModified();
+ SimpleDateFormat format;
+ // Temporarily commented out.. trying to fix Win7 display bug
+ if(System.currentTimeMillis() - mTime > MS_IN_SIX_MONTHS) {
+ // The mtime is less than 6 months ago
+ format = new SimpleDateFormat(" MMM dd HH:mm ", Locale.US);
+ } else {
+ // The mtime is more than 6 months ago
+ format = new SimpleDateFormat(" MMM dd yyyy ", Locale.US);
+ }
+ response.append(format.format(new Date(file.lastModified())));
+ response.append(lastNamePart);
+ response.append("\r\n");
+ return response.toString();
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdMKD.java b/qftplib/src/main/java/org/swiftp/server/CmdMKD.java
new file mode 100644
index 00000000..893765b6
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdMKD.java
@@ -0,0 +1,76 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import org.swiftp.Globals;
+
+import util.DocumentUtil;
+import util.FileUtil;
+
+public class CmdMKD extends FtpCmd implements Runnable {
+ String input;
+
+ public CmdMKD(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdMKD.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "MKD executing");
+ String param = getParameter(input);
+ File toCreate;
+ String errString = null;
+ mainblock: {
+ // If the param is an absolute path, use it as is. If it's a
+ // relative path, prepend the current working directory.
+ if(param.length() < 1) {
+ errString = "550 Invalid name\r\n";
+ break mainblock;
+ }
+ toCreate = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(toCreate)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainblock;
+ }
+ if(toCreate.exists()) {
+ errString = "550 Already exists\r\n";
+ break mainblock;
+ }
+ if(!FileUtil.mkdir(toCreate)) {
+ errString = "550 Error making directory (permissions?)\r\n";
+ break mainblock;
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, "MKD error: " + errString.trim());
+ } else {
+ sessionThread.writeString("250 Directory created\r\n");
+ }
+ myLog.l(Log.INFO, "MKD complete");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdMap.java b/qftplib/src/main/java/org/swiftp/server/CmdMap.java
new file mode 100644
index 00000000..b9c657a5
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdMap.java
@@ -0,0 +1,48 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+public class CmdMap {
+ protected Class extends FtpCmd> cmdClass;
+ String name;
+
+
+ public CmdMap(String name, Class extends FtpCmd> cmdClass) {
+ super();
+ this.name = name;
+ this.cmdClass = cmdClass;
+ }
+
+ public Class extends FtpCmd> getCommand() {
+ return cmdClass;
+ }
+
+ public void setCommand(Class extends FtpCmd> cmdClass) {
+ this.cmdClass = cmdClass;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdNLST.java b/qftplib/src/main/java/org/swiftp/server/CmdNLST.java
new file mode 100644
index 00000000..fd84a78a
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdNLST.java
@@ -0,0 +1,128 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+/* The code that is common to LIST and NLST is implemented in the abstract
+ * class CmdAbstractListing, which is inherited here.
+ * CmdLIST and CmdNLST just override the
+ * makeLsString() function in different ways to provide the different forms
+ * of output.
+ */
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import android.util.Log;
+
+public class CmdNLST extends CmdAbstractListing implements Runnable {
+ // The approximate number of milliseconds in 6 months
+ public final static long MS_IN_SIX_MONTHS = 6 * 30 * 24 * 60 * 60 * 1000;
+ private final String input;
+
+
+ public CmdNLST(SessionThread sessionThread, String input) {
+ super(sessionThread, input);
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String errString = null;
+
+ mainblock: {
+ String param = getParameter(input);
+ if(param.startsWith("-")) {
+ // Ignore options to list, which start with a dash
+ param = "";
+ }
+ File fileToList = null;
+ if(param.equals("")) {
+ fileToList = sessionThread.getWorkingDir();
+ } else {
+ if(param.contains("*")) {
+ errString = "550 NLST does not support wildcards\r\n";
+ break mainblock;
+ }
+ fileToList = new File(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(fileToList)) {
+ errString = "450 Listing target violates chroot\r\n";
+ break mainblock;
+ } else if(fileToList.isFile()) {
+ // Bernstein suggests that NLST should fail when a
+ // parameter is given and the parameter names a regular
+ // file (not a directory).
+ errString = "550 NLST for regular files is unsupported\r\n";
+ break mainblock;
+ }
+ }
+ String listing;
+ if(fileToList.isDirectory()) {
+ StringBuilder response = new StringBuilder();
+ errString = listDirectory(response, fileToList);
+ if(errString != null) {
+ break mainblock;
+ }
+ listing = response.toString();
+ } else {
+ listing = makeLsString(fileToList);
+ if(listing == null) {
+ errString = "450 Couldn't list that file\r\n";
+ break mainblock;
+ }
+ }
+ errString = sendListing(listing);
+ if(errString != null) {
+ break mainblock;
+ }
+ }
+
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.DEBUG, "NLST failed with: " + errString);
+ } else {
+ myLog.l(Log.DEBUG, "NLST completed OK");
+ }
+ // The success or error response over the control connection will
+ // have already been handled by sendListing, so we can just quit now.
+ }
+
+ @Override
+ protected String makeLsString(File file) {
+ if(!file.exists()) {
+ staticLog.l(Log.INFO, "makeLsString had nonexistent file");
+ return null;
+ }
+
+ // See Daniel Bernstein's explanation of NLST format at:
+ // http://cr.yp.to/ftp/list/binls.html
+ // This stuff is almost entirely based on his recommendations.
+
+ String lastNamePart = file.getName();
+ // Many clients can't handle files containing these symbols
+ if(lastNamePart.contains("*") ||
+ lastNamePart.contains("/"))
+ {
+ staticLog.l(Log.INFO, "Filename omitted due to disallowed character");
+ return null;
+ } else {
+ staticLog.l(Log.DEBUG, "Filename: " + lastNamePart );
+ return lastNamePart + "\r\n";
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdNOOP.java b/qftplib/src/main/java/org/swiftp/server/CmdNOOP.java
new file mode 100644
index 00000000..224cc0cc
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdNOOP.java
@@ -0,0 +1,36 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+
+public class CmdNOOP extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+
+ public CmdNOOP(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdNOOP.class.toString());
+ }
+
+ @Override
+ public void run() {
+ sessionThread.writeString("200 NOOP ok\r\n");
+ //myLog.l(Log.INFO, "Executing NOOP, done");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdOPTS.java b/qftplib/src/main/java/org/swiftp/server/CmdOPTS.java
new file mode 100644
index 00000000..b76644fe
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdOPTS.java
@@ -0,0 +1,76 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+
+public class CmdOPTS extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+ private final String input;
+
+ public CmdOPTS(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdOPTS.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String param = getParameter(input);
+ String errString = null;
+
+ mainBlock: {
+ if(param == null) {
+ errString = "550 Need argument to OPTS\r\n";
+ myLog.w("Couldn't understand empty OPTS command");
+ break mainBlock;
+ }
+ String[] splits = param.split(" ");
+ if(splits.length != 2) {
+ errString = "550 Malformed OPTS command\r\n";
+ myLog.w("Couldn't parse OPTS command");
+ break mainBlock;
+ }
+ String optName = splits[0].toUpperCase();
+ String optVal = splits[1].toUpperCase();
+ if(optName.equals("UTF8")) {
+ // OK, whatever. Don't really know what to do here. We
+ // always operate in UTF8 mode.
+ if(optVal.equals("ON")) {
+ myLog.d("Got OPTS UTF8 ON");
+ sessionThread.setEncoding("UTF-8");
+ } else {
+ myLog.i("Ignoring OPTS UTF8 for something besides ON");
+ }
+ break mainBlock;
+ } else {
+ myLog.d("Unrecognized OPTS option: " + optName);
+ errString = "502 Unrecognized option\r\n";
+ break mainBlock;
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.i("Template log message");
+ } else {
+ sessionThread.writeString("200 OPTS accepted\r\n");
+ myLog.d("Handled OPTS ok");
+ }
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdPASS.java b/qftplib/src/main/java/org/swiftp/server/CmdPASS.java
new file mode 100644
index 00000000..67d30755
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdPASS.java
@@ -0,0 +1,96 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import org.swiftp.Globals;
+import org.swiftp.R;
+import org.swiftp.Util;
+
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+public class CmdPASS extends FtpCmd implements Runnable {
+ String input;
+
+ public CmdPASS(SessionThread sessionThread, String input) {
+ // We can just discard the password for now. We're just
+ // following the expected dialogue, we're going to allow
+ // access in any case.
+ super(sessionThread, CmdPASS.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ // User must have already executed a USER command to
+ // populate the Account object's username
+ myLog.l(Log.DEBUG, "Executing PASS");
+
+ String attemptPassword = getParameter(input, true); // silent
+ String attemptUsername = sessionThread.account.getUsername();
+ if(attemptUsername == null) {
+ sessionThread.writeString("503 Must send USER first\r\n");
+ return;
+ }
+ Context ctx = Globals.getContext();
+ if(ctx == null) {
+ // This will probably never happen, since the global
+ // context is configured by the Service
+ myLog.l(Log.ERROR, "No global context in PASS\r\n");
+ }
+ String password;
+ String username;
+ SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ctx);
+ username = settings.getString(ctx.getString(R.string.key_username), "");
+ password = settings.getString(ctx.getString(R.string.key_ftp_pwd), "");
+// username = Util.getSP(ctx, "ftp.username");
+// password = Util.getSP(ctx, "ftp.pwd");
+
+ if (username.equals("")) {
+ username = Util.getCode(ctx);
+ }
+ if (password.equals("")) {
+ password = Util.getCode(ctx);
+ }
+ //username = settings.getString("username", null);
+ //password = settings.getString("password", null);
+ if(username == null || password == null) {
+ myLog.l(Log.ERROR, "Username or password misconfigured");
+ sessionThread.writeString("500 Internal error during authentication");
+ } else if(username.equals(attemptUsername) &&
+ password.equals(attemptPassword)) {
+ sessionThread.writeString("230 Access granted\r\n");
+ myLog.l(Log.INFO, "User " + username + " password verified");
+ sessionThread.authAttempt(true);
+ } else {
+ try {
+ // If the login failed, sleep for one second to foil
+ // brute force attacks
+ Thread.sleep(1000);
+ } catch(InterruptedException e) {}
+ myLog.l(Log.INFO, "Failed authentication");
+ sessionThread.writeString("530 Login incorrect.\r\n");
+ sessionThread.authAttempt(false);
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdPASV.java b/qftplib/src/main/java/org/swiftp/server/CmdPASV.java
new file mode 100644
index 00000000..e1da05a6
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdPASV.java
@@ -0,0 +1,72 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.net.InetAddress;
+
+import android.util.Log;
+
+public class CmdPASV extends FtpCmd implements Runnable {
+ //public static final String message = "TEMPLATE!!";
+
+ public CmdPASV(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdPASV.class.toString());
+ }
+
+ @Override
+ public void run() {
+ String cantOpen = "502 Couldn't open a port\r\n";
+ myLog.l(Log.DEBUG, "PASV running");
+ int port;
+ if((port = sessionThread.onPasv()) == 0) {
+ // There was a problem opening a port
+ myLog.l(Log.ERROR, "Couldn't open a port for PASV");
+ sessionThread.writeString(cantOpen);
+ return;
+ }
+ InetAddress addr = sessionThread.getDataSocketPasvIp();
+
+ if(addr == null) {
+ myLog.l(Log.ERROR, "PASV IP string invalid");
+ sessionThread.writeString(cantOpen);
+ return;
+ }
+ myLog.d("PASV sending IP: " + addr.getHostAddress());
+ if(port < 1) {
+ myLog.l(Log.ERROR, "PASV port number invalid");
+ sessionThread.writeString(cantOpen);
+ return;
+ }
+ StringBuilder response = new StringBuilder(
+ "227 Entering Passive Mode (");
+ // Output our IP address in the format xxx,xxx,xxx,xxx
+ response.append(addr.getHostAddress().replace('.', ','));
+ response.append(",");
+
+ // Output our port in the format p1,p2 where port=p1*256+p2
+ response.append(port / 256);
+ response.append(",");
+ response.append(port % 256);
+ response.append(").\r\n");
+ String responseString = response.toString();
+ sessionThread.writeString(responseString);
+ myLog.l(Log.DEBUG, "PASV completed, sent: " + responseString);
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdPORT.java b/qftplib/src/main/java/org/swiftp/server/CmdPORT.java
new file mode 100644
index 00000000..6eff5ee5
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdPORT.java
@@ -0,0 +1,98 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+import android.util.Log;
+
+public class CmdPORT extends FtpCmd implements Runnable {
+ //public static final String message = "TEMPLATE!!";
+ String input;
+
+ public CmdPORT(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdPORT.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "PORT running");
+ String errString = null;
+ mainBlock: {
+ String param = getParameter(input);
+ if(param.contains("|") && param.contains("::")) {
+ errString = "550 No IPv6 support, reconfigure your client\r\n";
+ break mainBlock;
+ }
+ String[] substrs = param.split(",");
+ if(substrs.length != 6) {
+ errString = "550 Malformed PORT argument\r\n";
+ break mainBlock;
+ }
+ for(int i=0; i 3)
+ {
+ errString = "550 Invalid PORT argument: " + substrs[i] +
+ "\r\n";
+ break mainBlock;
+ }
+ }
+ byte[] ipBytes = new byte[4];
+ for(int i=0; i<4; i++) {
+ try {
+ // We have to manually convert unsigned to signed
+ // byte representation.
+ int ipByteAsInt = Integer.parseInt(substrs[i]);
+ if(ipByteAsInt >= 128) {
+ ipByteAsInt -= 256;
+ }
+ ipBytes[i] = (byte)ipByteAsInt;
+ } catch (Exception e) {
+ errString = "550 Invalid PORT format: "
+ + substrs[i] + "\r\n";
+ break mainBlock;
+ }
+ }
+ InetAddress inetAddr;
+ try {
+ inetAddr = InetAddress.getByAddress(ipBytes);
+ } catch (UnknownHostException e) {
+ errString = "550 Unknown host\r\n";
+ break mainBlock;
+ }
+
+ int port = Integer.parseInt(substrs[4]) * 256 +
+ Integer.parseInt(substrs[5]);
+
+ sessionThread.onPort(inetAddr, port);
+ }
+ if(errString == null) {
+ sessionThread.writeString("200 PORT OK\r\n");
+ myLog.l(Log.DEBUG, "PORT completed");
+ } else {
+ myLog.l(Log.INFO, "PORT error: " + errString);
+ sessionThread.writeString(errString);
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdPWD.java b/qftplib/src/main/java/org/swiftp/server/CmdPWD.java
new file mode 100644
index 00000000..5ae93711
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdPWD.java
@@ -0,0 +1,64 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.IOException;
+
+import org.swiftp.Globals;
+
+import android.util.Log;
+
+public class CmdPWD extends FtpCmd implements Runnable {
+// public static final String message = "TEMPLATE!!";
+
+ public CmdPWD(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdPWD.class.toString());
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "PWD executing");
+
+ // We assume that the chroot restriction has been applied, and that
+ // therefore the current directory is located somewhere within the
+ // chroot directory. Therefore, we can just slice of the chroot
+ // part of the current directory path in order to get the
+ // user-visible path (inside the chroot directory).
+ try {
+ String currentDir = sessionThread.getWorkingDir().getCanonicalPath();
+ currentDir = currentDir.substring(Globals.getChrootDir().
+ getCanonicalPath().length());
+ // The root directory requires special handling to restore its
+ // leading slash
+ if(currentDir.length() == 0) {
+ currentDir = "/";
+ }
+ sessionThread.writeString("257 \""
+ + currentDir
+ + "\"\r\n");
+ } catch (IOException e) {
+ // This shouldn't happen unless our input validation has failed
+ myLog.l(Log.ERROR, "PWD canonicalize");
+ sessionThread.closeSocket(); // should cause thread termination
+ }
+ myLog.l(Log.DEBUG, "PWD complete");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdQUIT.java b/qftplib/src/main/java/org/swiftp/server/CmdQUIT.java
new file mode 100644
index 00000000..23412d3f
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdQUIT.java
@@ -0,0 +1,38 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdQUIT extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+
+ public CmdQUIT(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdQUIT.class.toString());
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "QUITting");
+ sessionThread.writeString("221 Goodbye\r\n");
+ sessionThread.closeSocket();
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdRETR.java b/qftplib/src/main/java/org/swiftp/server/CmdRETR.java
new file mode 100644
index 00000000..31bde621
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdRETR.java
@@ -0,0 +1,158 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import org.swiftp.Defaults;
+
+import android.util.Log;
+
+public class CmdRETR extends FtpCmd implements Runnable {
+ //public static final String message = "TEMPLATE!!";
+ protected String input;
+
+ public CmdRETR(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdRETR.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "RETR executing");
+ String param = getParameter(input);
+ File fileToRetr;
+ String errString = null;
+
+ mainblock: {
+ fileToRetr = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(fileToRetr)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainblock;
+ } else if(fileToRetr.isDirectory()) {
+ myLog.l(Log.DEBUG, "Ignoring RETR for directory");
+ errString = "550 Can't RETR a directory\r\n";
+ break mainblock;
+ } else if(!fileToRetr.exists()) {
+ myLog.l(Log.INFO, "Can't RETR nonexistent file: " +
+ fileToRetr.getAbsolutePath());
+ errString = "550 File does not exist\r\n";
+ break mainblock;
+ } else if(!fileToRetr.canRead()) {
+ myLog.l(Log.INFO, "Failed RETR permission (canRead() is false)");
+ errString = "550 No read permissions\r\n";
+ break mainblock;
+ } /*else if(!sessionThread.isBinaryMode()) {
+ myLog.l(Log.INFO, "Failed RETR in text mode");
+ errString = "550 Text mode RETR not supported\r\n";
+ break mainblock;
+ }*/
+ FileInputStream in = null;
+ try {
+ in = new FileInputStream(fileToRetr);
+ byte[] buffer = new byte[Defaults.getDataChunkSize()];
+ int bytesRead;
+ if(sessionThread.startUsingDataSocket()) {
+ myLog.l(Log.DEBUG, "RETR opened data socket");
+ } else {
+ errString = "425 Error opening socket\r\n";
+ myLog.l(Log.INFO, "Error in initDataSocket()");
+ break mainblock;
+ }
+ sessionThread.writeString("150 Sending file\r\n");
+ if(sessionThread.isBinaryMode()) {
+ myLog.l(Log.DEBUG, "Transferring in binary mode");
+ while((bytesRead = in.read(buffer)) != -1) {
+ //myLog.l(Log.DEBUG,
+ // String.format("CmdRETR sending %d bytes", bytesRead));
+ if(sessionThread
+ .sendViaDataSocket(buffer, bytesRead) == false)
+ {
+ errString = "426 Data socket error\r\n";
+ myLog.l(Log.INFO, "Data socket error");
+ break mainblock;
+ }
+ }
+ } else { // We're in ASCII mode
+ myLog.l(Log.DEBUG, "Transferring in ASCII mode");
+ // We have to convert all solitary \n to \r\n
+ boolean lastBufEndedWithCR = false;
+ while((bytesRead = in.read(buffer)) != -1) {
+ int startPos = 0, endPos = 0;
+ byte[] crnBuf = {'\r','\n'};
+ for(endPos = 0; endPos .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import org.swiftp.Globals;
+
+import util.DocumentUtil;
+import util.FileUtil;
+
+public class CmdRMD extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+ protected String input;
+
+ public CmdRMD(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdRMD.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.INFO, "RMD executing");
+ String param = getParameter(input);
+ File toRemove;
+ String errString = null;
+ mainblock: {
+ if(param.length() < 1) {
+ errString = "550 Invalid argument\r\n";
+ break mainblock;
+ }
+ toRemove = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(toRemove)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainblock;
+ }
+ if(!toRemove.isDirectory()) {
+ errString = "550 Can't RMD a non-directory\r\n";
+ break mainblock;
+ }
+ if(toRemove.equals(new File("/"))) {
+ errString = "550 Won't RMD the root directory\r\n";
+ break mainblock;
+ }
+ if(!FileUtil.delete(toRemove)) {
+ errString = "550 Deletion error, possibly incomplete\r\n";
+ break mainblock;
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, "RMD failed: " + errString.trim());
+ } else {
+ sessionThread.writeString("250 Removed directory\r\n");
+ }
+ myLog.l(Log.DEBUG, "RMD finished");
+ }
+
+ /* *
+ * Accepts a file or directory name, and recursively deletes the contents
+ * of that directory and all subdirectories.
+ * @param toDelete
+ * @return Whether the operation completed successfully
+ * /
+ protected boolean recursiveDelete(File toDelete) {
+ if(!toDelete.exists()) {
+ return false;
+ }
+ if(toDelete.isDirectory()) {
+ // If any of the recursive operations fail, then we return false
+ boolean success = true;
+ for(File entry : toDelete.listFiles()) {
+ success &= recursiveDelete(entry);
+ }
+ myLog.l(Log.DEBUG, "Recursively deleted: " + toDelete);
+ return success && toDelete.delete();
+ } else {
+ myLog.l(Log.DEBUG, "RMD deleting file: " + toDelete);
+ return toDelete.delete();
+ }
+ }*/
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdRNFR.java b/qftplib/src/main/java/org/swiftp/server/CmdRNFR.java
new file mode 100644
index 00000000..5a3c31c0
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdRNFR.java
@@ -0,0 +1,58 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import android.util.Log;
+
+public class CmdRNFR extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdRNFR(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdRNFR.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String param = getParameter(input);
+ String errString = null;
+ File file = null;
+ mainblock: {
+ file = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ if(violatesChroot(file)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainblock;
+ }
+ if(!file.exists()) {
+ errString = "450 Cannot rename nonexistent file\r\n";
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, "RNFR failed: " + errString.trim());
+ sessionThread.setRenameFrom(null);
+ } else {
+ sessionThread.writeString("350 Filename noted, now send RNTO\r\n");
+ sessionThread.setRenameFrom(file);
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdRNTO.java b/qftplib/src/main/java/org/swiftp/server/CmdRNTO.java
new file mode 100644
index 00000000..ceb244e7
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdRNTO.java
@@ -0,0 +1,73 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.File;
+
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import org.swiftp.Globals;
+
+import util.DocumentUtil;
+import util.FileUtil;
+
+public class CmdRNTO extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdRNTO(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdRNTO.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String param = getParameter(input);
+ String errString = null;
+ File toFile = null;
+ myLog.l(Log.DEBUG, "RNTO executing\r\n");
+ mainblock: {
+ myLog.l(Log.INFO, "param: " + param);
+ toFile = inputPathToChrootedFile(sessionThread.getWorkingDir(), param);
+ myLog.l(Log.INFO, "RNTO parsed: " + toFile.getPath());
+ if(violatesChroot(toFile)) {
+ errString = "550 Invalid name or chroot violation\r\n";
+ break mainblock;
+ }
+ File fromFile = sessionThread.getRenameFrom();
+ if(fromFile == null) {
+ errString = "550 Rename error, maybe RNFR not sent\r\n";
+ break mainblock;
+ }
+ if(!FileUtil.rename(fromFile,toFile)) {
+ errString = "550 Error during rename operation\r\n";
+ break mainblock;
+ }
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ myLog.l(Log.INFO, "RNFR failed: " + errString.trim());
+ } else {
+ sessionThread.writeString("250 rename successful\r\n");
+ }
+ sessionThread.setRenameFrom(null);
+ myLog.l(Log.DEBUG, "RNTO finished");
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdSIZE.java b/qftplib/src/main/java/org/swiftp/server/CmdSIZE.java
new file mode 100644
index 00000000..933beb54
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdSIZE.java
@@ -0,0 +1,56 @@
+package org.swiftp.server;
+
+import java.io.File;
+import java.io.IOException;
+
+public class CmdSIZE extends FtpCmd {
+ protected String input;
+
+ public CmdSIZE(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdSIZE.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ myLog.d("SIZE executing");
+
+ String errString = null;
+ String param = getParameter(input);
+ long size = 0;
+ mainblock: {
+ File currentDir = sessionThread.getWorkingDir();
+ if(param.contains(File.separator)) {
+ errString = "550 No directory traversal allowed in SIZE param\r\n";
+ break mainblock;
+ }
+ File target = new File(currentDir, param);
+
+ // We should have caught any invalid location access before now, but
+ // here we check again, just to be explicitly sure.
+ if(violatesChroot(target)) {
+ errString = "550 SIZE target violates chroot\r\n";
+ break mainblock;
+ }
+ if(!target.exists()) {
+ errString = "550 Cannot get the SIZE of nonexistent object\r\n";
+ try {
+ myLog.i("Failed getting size of: " + target.getCanonicalPath());
+ } catch (IOException e) {}
+ break mainblock;
+ }
+ if(!target.isFile()) {
+ errString = "550 Cannot get the size of a non-file\r\n";
+ break mainblock;
+ }
+ size = target.length();
+ }
+ if(errString != null) {
+ sessionThread.writeString(errString);
+ } else {
+ sessionThread.writeString("213 " + size + "\r\n");
+ }
+ myLog.d("SIZE complete");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdSTOR.java b/qftplib/src/main/java/org/swiftp/server/CmdSTOR.java
new file mode 100644
index 00000000..f5015883
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdSTOR.java
@@ -0,0 +1,35 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+
+public class CmdSTOR extends CmdAbstractStore implements Runnable {
+ protected String input;
+
+ public CmdSTOR(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdSTOR.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ doStorOrAppe(getParameter(input), false);
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdSYST.java b/qftplib/src/main/java/org/swiftp/server/CmdSYST.java
new file mode 100644
index 00000000..39b1fc91
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdSYST.java
@@ -0,0 +1,40 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdSYST extends FtpCmd implements Runnable {
+ // This is considered a safe response to the SYST command, see
+ // http://cr.yp.to/ftp/syst.html
+ public static final String response = "215 UNIX Type: L8\r\n";
+
+ public CmdSYST(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdSYST.class.toString());
+ }
+
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "SYST executing");
+ sessionThread.writeString(response);
+ myLog.l(Log.DEBUG, "SYST finished");
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdTYPE.java b/qftplib/src/main/java/org/swiftp/server/CmdTYPE.java
new file mode 100644
index 00000000..e60c66cc
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdTYPE.java
@@ -0,0 +1,50 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdTYPE extends FtpCmd implements Runnable {
+ String input;
+
+ public CmdTYPE(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdTYPE.class.toString());
+ this.input = input;
+ }
+
+ @Override
+ public void run() {
+ String output;
+ myLog.l(Log.DEBUG, "TYPE executing");
+ String param = getParameter(input);
+ if(param.equals("I") || param.equals("L 8")) {
+ output = "200 Binary type set\r\n";
+ sessionThread.setBinaryMode(true);
+ } else if (param.equals("A") || param.equals("A N")) {
+ output = "200 ASCII type set\r\n";
+ sessionThread.setBinaryMode(false);
+ } else {
+ output = "503 Malformed TYPE command\r\n";
+ }
+ sessionThread.writeString(output);
+ myLog.l(Log.DEBUG, "TYPE complete");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdTemplate.java b/qftplib/src/main/java/org/swiftp/server/CmdTemplate.java
new file mode 100644
index 00000000..4f9a4b96
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdTemplate.java
@@ -0,0 +1,37 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdTemplate extends FtpCmd implements Runnable {
+ public static final String message = "TEMPLATE!!";
+
+ public CmdTemplate(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdTemplate.class.toString());
+ }
+
+ @Override
+ public void run() {
+ sessionThread.writeString(message);
+ myLog.l(Log.INFO, "Template log message");
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/CmdUSER.java b/qftplib/src/main/java/org/swiftp/server/CmdUSER.java
new file mode 100644
index 00000000..338fc924
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/CmdUSER.java
@@ -0,0 +1,45 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import android.util.Log;
+
+public class CmdUSER extends FtpCmd implements Runnable {
+ protected String input;
+
+ public CmdUSER(SessionThread sessionThread, String input) {
+ super(sessionThread, CmdUSER.class.toString());
+ this.input = input;
+
+ }
+
+ @Override
+ public void run() {
+ myLog.l(Log.DEBUG, "USER executing");
+ String username = FtpCmd.getParameter(input);
+ if(!username.matches("[A-Za-z0-9]+")) {
+ sessionThread.writeString("530 Invalid username\r\n");
+ return;
+ }
+ sessionThread.writeString("331 Send password\r\n");
+ sessionThread.account.setUsername(username);
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/DataSocketFactory.java b/qftplib/src/main/java/org/swiftp/server/DataSocketFactory.java
new file mode 100644
index 00000000..1a25c824
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/DataSocketFactory.java
@@ -0,0 +1,60 @@
+package org.swiftp.server;
+
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.swiftp.MyLog;
+
+
+abstract public class DataSocketFactory {
+
+ /**
+ * A DataSocketFactory hides the implementation of the opening and closing
+ * of the data sockets which are used to transmit directory listings and
+ * file contents. This is necessary because normal FTP data sockets are
+ * opened and closed very differently from the abnormal sort of data sockets
+ * we use in conjunction with our proxy system.
+ */
+ protected MyLog myLog = new MyLog(getClass().getName());
+
+ /**
+ * When SwiFTP receives a PORT command, this will be called. Subclasses should
+ * perform whatever initialization is necessary.
+ * @return Whether the necessary actions completed successfully
+ */
+ abstract public boolean onPort(InetAddress dest, int port);
+
+ /**
+ * When SwiFTP receives a PASV command, this will be called. Subclasses should
+ * perform whatever initialization is necessary.
+ * @return Whether the necessary actions completed successfully
+ */
+ abstract public int onPasv();
+
+ /**
+ * When it's time for data transfer to begin, the SessionThread will call this
+ * method to perform any necessary actions to prepare the Socket for use and
+ * return it in a state that's ready for reading or writing.
+ * @return The opened Socket
+ */
+ abstract public Socket onTransfer();
+
+ /**
+ * Sometimes we'll need to know the IP address at which we can be contacted. For
+ * instance, the response to a PASV command will be the IP and port that the
+ * client should use to connect it's data socket.
+ */
+ abstract public InetAddress getPasvIp();
+
+ /**
+ * We sometimes want to track the total number of bytes that go over the
+ * command and data sockets. The SessionThread can call this function to
+ * reports its usage, and different DataSocketFactory subclasses can
+ * handle the data however is appropriate. For the ProxyDataSocketFactory,
+ * we want to present the total to the user in the UI to guilt them into
+ * donating.
+ * @param numBytes the number of bytes to add to the total
+ */
+ abstract public void reportTraffic(long numBytes);
+}
+
diff --git a/qftplib/src/main/java/org/swiftp/server/FtpCmd.java b/qftplib/src/main/java/org/swiftp/server/FtpCmd.java
new file mode 100644
index 00000000..615e35fb
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/FtpCmd.java
@@ -0,0 +1,207 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+
+import java.io.File;
+import java.lang.reflect.Constructor;
+
+import org.swiftp.Globals;
+import org.swiftp.MyLog;
+
+import android.util.Log;
+
+public abstract class FtpCmd implements Runnable {
+ protected SessionThread sessionThread;
+ protected MyLog myLog;
+ protected static MyLog staticLog = new MyLog(FtpCmd.class.toString());
+
+ protected static CmdMap[] cmdClasses = {
+ new CmdMap("SYST", CmdSYST.class),
+ new CmdMap("USER", CmdUSER.class),
+ new CmdMap("PASS", CmdPASS.class),
+ new CmdMap("TYPE", CmdTYPE.class),
+ new CmdMap("CWD", CmdCWD.class),
+ new CmdMap("PWD", CmdPWD.class),
+ new CmdMap("LIST", CmdLIST.class),
+ new CmdMap("PASV", CmdPASV.class),
+ new CmdMap("RETR", CmdRETR.class),
+ new CmdMap("NLST", CmdNLST.class),
+ new CmdMap("NOOP", CmdNOOP.class),
+ new CmdMap("STOR", CmdSTOR.class),
+ new CmdMap("DELE", CmdDELE.class),
+ new CmdMap("RNFR", CmdRNFR.class),
+ new CmdMap("RNTO", CmdRNTO.class),
+ new CmdMap("RMD", CmdRMD.class),
+ new CmdMap("MKD", CmdMKD.class),
+ new CmdMap("OPTS", CmdOPTS.class),
+ new CmdMap("PORT", CmdPORT.class),
+ new CmdMap("QUIT", CmdQUIT.class),
+ new CmdMap("FEAT", CmdFEAT.class),
+ new CmdMap("SIZE", CmdSIZE.class),
+ new CmdMap("CDUP", CmdCDUP.class),
+ new CmdMap("APPE", CmdAPPE.class),
+ new CmdMap("XCUP", CmdCDUP.class), // synonym
+ new CmdMap("XPWD", CmdPWD.class), // synonym
+ new CmdMap("XMKD", CmdMKD.class), // synonym
+ new CmdMap("XRMD", CmdRMD.class) // synonym
+ };
+
+ public FtpCmd(SessionThread sessionThread, String logName) {
+ this.sessionThread = sessionThread;
+ myLog = new MyLog(logName);
+ }
+
+ abstract public void run();
+
+ protected static void dispatchCommand(SessionThread session,
+ String inputString) {
+ String[] strings = inputString.split(" ");
+ String unrecognizedCmdMsg = "502 Command not recognized\r\n";
+ if(strings == null) {
+ // There was some egregious sort of parsing error
+ String errString = "502 Command parse error\r\n";
+ staticLog.l(Log.INFO, errString);
+ session.writeString(errString);
+ return;
+ }
+ if(strings.length < 1) {
+ staticLog.l(Log.INFO, "No strings parsed");
+ session.writeString(unrecognizedCmdMsg);
+ return;
+ }
+ String verb = strings[0];
+ if(verb.length() < 1) {
+ staticLog.l(Log.INFO, "Invalid command verb");
+ session.writeString(unrecognizedCmdMsg);
+ return;
+ }
+ FtpCmd cmdInstance = null;
+ verb = verb.trim();
+ verb = verb.toUpperCase();
+ for(int i=0; i constructor;
+ try {
+ constructor = cmdClasses[i].getCommand().getConstructor(
+ new Class[] {SessionThread.class, String.class});
+ } catch (NoSuchMethodException e) {
+ staticLog.l(Log.ERROR, "FtpCmd subclass lacks expected " +
+ "constructor ");
+ return;
+ }
+ try {
+ cmdInstance = constructor.newInstance(
+ new Object[] {session, inputString});
+ } catch(Exception e) {
+ staticLog.l(Log.ERROR,
+ "Instance creation error on FtpCmd");
+ return;
+ }
+ }
+ }
+ if(cmdInstance == null) {
+ // If we couldn't find a matching command,
+ staticLog.l(Log.DEBUG, "Ignoring unrecognized FTP verb: " + verb);
+ session.writeString(unrecognizedCmdMsg);
+ return;
+ } else if(session.isAuthenticated()
+ || cmdInstance.getClass().equals(CmdUSER.class)
+ || cmdInstance.getClass().equals(CmdPASS.class)
+ || cmdInstance.getClass().equals(CmdUSER.class))
+ {
+ // Unauthenticated users can run only USER, PASS and QUIT
+ cmdInstance.run();
+ } else {
+ session.writeString("530 Login first with USER and PASS\r\n");
+ }
+ }
+
+ /**
+ * An FTP parameter is that part of the input string that occurs
+ * after the first space, including any subsequent spaces. Also,
+ * we want to chop off the trailing '\r\n', if present.
+ *
+ * Some parameters shouldn't be logged or output (e.g. passwords),
+ * so the caller can use silent==true in that case.
+ */
+ static public String getParameter(String input, boolean silent) {
+ if(input == null) {
+ return "";
+ }
+ int firstSpacePosition = input.indexOf(' ');
+ if(firstSpacePosition == -1) {
+ return "";
+ }
+ String retString = input.substring(firstSpacePosition+1);
+
+ // Remove trailing whitespace
+ // todo: trailing whitespace may be significant, just remove \r\n
+ retString = retString.replaceAll("\\s+$", "");
+
+ if(!silent) {
+ staticLog.l(Log.DEBUG, "Parsed argument: " + retString);
+ }
+ return retString;
+ }
+
+ /**
+ * A wrapper around getParameter, for when we don't want it to be silent.
+ */
+ static public String getParameter(String input) {
+ return getParameter(input, false);
+ }
+
+ public static File inputPathToChrootedFile(File existingPrefix, String param) {
+ try {
+ if(param.charAt(0) == '/') {
+ // The STOR contained an absolute path
+ File chroot = Globals.getChrootDir();
+ return new File(chroot, param);
+ }
+ } catch (Exception e) {}
+
+ // The STOR contained a relative path
+ return new File(existingPrefix, param);
+ }
+
+ public boolean violatesChroot(File file) {
+ File chroot = Globals.getChrootDir();
+ try {
+ String canonicalPath = file.getCanonicalPath();
+ if(!canonicalPath.startsWith(chroot.toString())) {
+ myLog.l(Log.INFO, "Path violated folder restriction, denying");
+ myLog.l(Log.DEBUG, "path: " + canonicalPath);
+ myLog.l(Log.DEBUG, "chroot: " + chroot.toString());
+ return true; // the path must begin with the chroot path
+ }
+ return false;
+ } catch(Exception e) {
+ myLog.l(Log.INFO, "Path canonicalization problem: " + e.toString());
+ myLog.l(Log.INFO, "When checking file: " + file.getAbsolutePath());
+ return true; // for security, assume violation
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/NormalDataSocketFactory.java b/qftplib/src/main/java/org/swiftp/server/NormalDataSocketFactory.java
new file mode 100644
index 00000000..a967a660
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/NormalDataSocketFactory.java
@@ -0,0 +1,161 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.swiftp.Defaults;
+import org.swiftp.FTPServerService;
+
+import android.util.Log;
+
+public class NormalDataSocketFactory extends DataSocketFactory {
+ /**
+ * This class implements normal, traditional opening and closing of data sockets
+ * used for transmitting directory listings and file contents. PORT and PASV
+ * work according to the FTP specs. This is in contrast to a
+ * ProxyDataSocketFactory, which performs contortions to allow data sockets
+ * to be proxied through a server out in the cloud.
+ *
+ */
+
+ // Listener socket used for PASV mode
+ ServerSocket server = null;
+ // Remote IP & port information used for PORT mode
+ InetAddress remoteAddr;
+ int remotePort;
+ boolean isPasvMode = true;
+
+ public NormalDataSocketFactory() {
+ clearState();
+ }
+
+
+ private void clearState() {
+ /**
+ * Clears the state of this object, as if no pasv() or port() had occurred.
+ * All sockets are closed.
+ */
+ if(server != null) {
+ try {
+ server.close();
+ } catch (IOException e) {}
+ }
+ server = null;
+ remoteAddr = null;
+ remotePort = 0;
+ myLog.l(Log.DEBUG, "NormalDataSocketFactory state cleared");
+ }
+
+ @Override
+ public int onPasv() {
+ clearState();
+ try {
+ // Listen on any port (port parameter 0)
+ server = new ServerSocket(0, Defaults.tcpConnectionBacklog);
+ myLog.l(Log.DEBUG, "Data socket pasv() listen successful");
+ return server.getLocalPort();
+ } catch(IOException e) {
+ myLog.l(Log.ERROR, "Data socket creation error");
+ clearState();
+ return 0;
+ }
+ }
+
+ @Override
+ public boolean onPort(InetAddress remoteAddr, int remotePort) {
+ clearState();
+ this.remoteAddr = remoteAddr;
+ this.remotePort = remotePort;
+ return true;
+ }
+
+ @Override
+ public Socket onTransfer() {
+ if(server == null) {
+ // We're in PORT mode (not PASV)
+ if(remoteAddr == null || remotePort == 0) {
+ myLog.l(Log.INFO, "PORT mode but not initialized correctly");
+ clearState();
+ return null;
+ }
+ Socket socket;
+ try {
+ socket = new Socket(remoteAddr, remotePort);
+ } catch (IOException e) {
+ myLog.l(Log.INFO,
+ "Couldn't open PORT data socket to: " +
+ remoteAddr.toString() + ":" + remotePort);
+ clearState();
+ return null;
+ }
+
+ // Kill the socket if nothing happens for X milliseconds
+ try {
+ socket.setSoTimeout(Defaults.SO_TIMEOUT_MS);
+ } catch (Exception e) {
+ myLog.l(Log.ERROR, "Couldn't set SO_TIMEOUT");
+ clearState();
+ return null;
+ }
+
+ return socket;
+ } else {
+ // We're in PASV mode (not PORT)
+ Socket socket = null;
+ try {
+ socket = server.accept();
+ myLog.l(Log.DEBUG, "onTransfer pasv accept successful");
+ } catch (Exception e) {
+ myLog.l(Log.INFO, "Exception accepting PASV socket");
+ socket = null;
+ }
+ clearState();
+ return socket; // will be null if error occurred
+ }
+ }
+
+ /**
+ * Return the port number that the remote client should be informed of (in the body
+ * of the PASV response).
+ * @return The port number, or -1 if error.
+ */
+ public int getPortNumber() {
+ if(server != null) {
+ return server.getLocalPort(); // returns -1 if serversocket is unbound
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public InetAddress getPasvIp() {
+ //String retVal = server.getInetAddress().getHostAddress();
+ return FTPServerService.getWifiIp();
+ }
+
+ @Override
+ public void reportTraffic(long bytes) {
+ // ignore, we don't care about how much traffic goes over wifi.
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/ProxyConnector.java b/qftplib/src/main/java/org/swiftp/server/ProxyConnector.java
new file mode 100644
index 00000000..e0f264ba
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/ProxyConnector.java
@@ -0,0 +1,769 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+ */
+
+package org.swiftp.server;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.swiftp.Defaults;
+import org.swiftp.FTPServerService;
+import org.swiftp.Globals;
+import org.swiftp.MyLog;
+import org.swiftp.R;
+import org.swiftp.Util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+public class ProxyConnector extends Thread {
+ public static final int IN_BUF_SIZE = 2048;
+ public static final String ENCODING = "UTF-8";
+ public static final int RESPONSE_WAIT_MS = 10000;
+ public static final int QUEUE_WAIT_MS = 20000;
+ public static final long UPDATE_USAGE_BYTES = 5000000;
+ public static final String PREFERRED_SERVER = "preferred_server"; // preferences
+ public static final int CONNECT_TIMEOUT = 5000;
+
+ private final FTPServerService ftpServerService;
+ private final MyLog myLog = new MyLog(getClass().getName());
+ private JSONObject response = null;
+ private Thread responseWaiter = null;
+ private final Queue queuedRequestThreads = new LinkedList();
+ private Socket commandSocket = null;
+ private OutputStream out = null;
+ private String hostname = null;
+ private InputStream inputStream = null;
+ private long proxyUsage = 0;
+ private State proxyState = State.DISCONNECTED;
+ private String prefix;
+ private String proxyMessage = null;
+
+ public enum State {
+ CONNECTING, CONNECTED, FAILED, UNREACHABLE, DISCONNECTED
+ };
+
+ // QuotaStats cachedQuotaStats = null; // quotas have been canceled for now
+
+ static final String USAGE_PREFS_NAME = "proxy_usage_data";
+
+ /*
+ * We establish a so-called "command session" to the proxy. New connections will be
+ * handled by creating addition control and data connections to the proxy. See
+ * proxy_protocol.txt and proxy_architecture.pdf for an explanation of how proxying
+ * works. Hint: it's complicated.
+ */
+
+ public ProxyConnector(FTPServerService ftpServerService) {
+ this.ftpServerService = ftpServerService;
+ this.proxyUsage = getPersistedProxyUsage();
+ setProxyState(State.DISCONNECTED);
+ Globals.setProxyConnector(this);
+ }
+
+ @Override
+ public void run() {
+ myLog.i("In ProxyConnector.run()");
+ setProxyState(State.CONNECTING);
+ try {
+ String candidateProxies[] = getProxyList();
+ for (String candidateHostname : candidateProxies) {
+ hostname = candidateHostname;
+ commandSocket = newAuthedSocket(hostname, Defaults.REMOTE_PROXY_PORT);
+ if (commandSocket == null) {
+ continue;
+ }
+ commandSocket.setSoTimeout(0); // 0 == forever
+ // commandSocket.setKeepAlive(true);
+ // Now that we have authenticated, we want to start the command session so
+ // we can
+ // be notified of pending control sessions.
+ JSONObject request = makeJsonRequest("start_command_session");
+ response = sendRequest(commandSocket, request);
+ if (response == null) {
+ myLog.i("Couldn't create proxy command session");
+ continue; // try next server
+ }
+ if (!response.has("prefix")) {
+ myLog.l(Log.INFO,
+ "start_command_session didn't receive a prefix in response");
+ continue; // try next server
+ }
+ prefix = response.getString("prefix");
+ response = null; // Indicate that response is free for other use
+ myLog.l(Log.INFO, "Got prefix of: " + prefix);
+ break; // breaking with commandSocket != null indicates success
+ }
+ if (commandSocket == null) {
+ myLog.l(Log.INFO, "No proxies accepted connection, failing.");
+ setProxyState(State.UNREACHABLE);
+ return;
+ }
+ setProxyState(State.CONNECTED);
+ preferServer(hostname);
+ inputStream = commandSocket.getInputStream();
+ out = commandSocket.getOutputStream();
+ int numBytes;
+ byte[] bytes = new byte[IN_BUF_SIZE];
+ // spawnQuotaRequester().start();
+ while (true) {
+ myLog.d("to proxy read()");
+ numBytes = inputStream.read(bytes);
+ incrementProxyUsage(numBytes);
+ myLog.d("from proxy read()");
+ JSONObject incomingJson = null;
+ if (numBytes > 0) {
+ String responseString = new String(bytes, ENCODING);
+ incomingJson = new JSONObject(responseString);
+ if (incomingJson.has("action")) {
+ // If the incoming JSON object has an "action" field, then it is a
+ // request, and not a response
+ incomingCommand(incomingJson);
+ } else {
+ // If the incoming JSON object does not have an "action" field,
+ // then
+ // it is a response to a request we sent earlier.
+ // If there's an object waiting for a response, then that object
+ // will be referenced by responseWaiter.
+ if (responseWaiter != null) {
+ if (response != null) {
+ myLog.l(Log.INFO,
+ "Overwriting existing cmd session response");
+ }
+ response = incomingJson;
+ responseWaiter.interrupt();
+ } else {
+ myLog.l(Log.INFO, "Response received but no responseWaiter");
+ }
+ }
+ } else if (numBytes == 0) {
+ myLog.d("Command socket read 0 bytes, looping");
+ } else { // numBytes < 0
+ myLog.l(Log.DEBUG, "Command socket end of stream, exiting");
+ if (proxyState != State.DISCONNECTED) {
+ // Set state to FAILED unless this was an intentional
+ // socket closure.
+ setProxyState(State.FAILED);
+ }
+ break;
+ }
+ }
+ myLog.l(Log.INFO, "ProxyConnector thread quitting cleanly");
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "IOException in command session: " + e);
+ setProxyState(State.FAILED);
+ } catch (JSONException e) {
+ myLog.l(Log.INFO, "Commmand socket JSONException: " + e);
+ setProxyState(State.FAILED);
+ } catch (Exception e) {
+ myLog.l(Log.INFO, "Other exception in ProxyConnector: " + e);
+ setProxyState(State.FAILED);
+ } finally {
+ Globals.setProxyConnector(null);
+ hostname = null;
+ myLog.d("ProxyConnector.run() returning");
+ persistProxyUsage();
+ }
+ }
+
+ // This function is used to spawn a new Thread that will make a request over the
+ // command thread. Since the main ProxyConnector thread handles the input
+ // request/response de-multiplexing, it cannot also make a request using the
+ // sendCmdSocketRequest, since sendCmdSocketRequest will block waiting for
+ // a response, but the same thread is expected to deliver the response.
+ // The short story is, if the main ProxyConnector command session thread wants to
+ // make a request, the easiest way is to spawn a new thread and have it call
+ // sendCmdSocketRequest in the same way as any other thread.
+ // private Thread spawnQuotaRequester() {
+ // return new Thread() {
+ // public void run() {
+ // getQuotaStats(false);
+ // }
+ // };
+ // }
+
+ /**
+ * Since we want devices to generally stick with the same proxy server, and we may
+ * want to explicitly redirect some devices to other servers, we have this mechanism
+ * to store a "preferred server" on the device.
+ */
+ private void preferServer(String hostname) {
+ SharedPreferences prefs = Globals.getContext().getSharedPreferences(
+ PREFERRED_SERVER, 0);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PREFERRED_SERVER, hostname);
+ editor.commit();
+ }
+
+ private String[] getProxyList() {
+ SharedPreferences prefs = Globals.getContext().getSharedPreferences(
+ PREFERRED_SERVER, 0);
+ String preferred = prefs.getString(PREFERRED_SERVER, null);
+
+ String[] allProxies;
+
+ if (Defaults.release) {
+ allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org",
+ "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org",
+ "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" };
+ } else {
+ // allProxies = new String[] {
+ // "cdev.swiftp.org"
+ // };
+ allProxies = new String[] { "c1.swiftp.org", "c2.swiftp.org",
+ "c3.swiftp.org", "c4.swiftp.org", "c5.swiftp.org", "c6.swiftp.org",
+ "c7.swiftp.org", "c8.swiftp.org", "c9.swiftp.org" };
+ }
+
+ // We should randomly permute the server list in order to spread
+ // load between servers. Collections offers a shuffle() function
+ // that does this, so we'll convert to List and back to String[].
+ List proxyList = Arrays.asList(allProxies);
+ Collections.shuffle(proxyList);
+ allProxies = proxyList.toArray(new String[] {}); // arg used for type
+
+ // Return preferred server first, followed by all others
+ if (preferred == null) {
+ return allProxies;
+ } else {
+ return Util.concatStrArrays(new String[] { preferred }, allProxies);
+ }
+ }
+
+ private boolean checkAndPrintJsonError(JSONObject json) throws JSONException {
+ if (json.has("error_code")) {
+ // The returned JSON object will have a field called "errorCode"
+ // if there was a problem executing our request.
+ StringBuilder s = new StringBuilder("Error in JSON response, code: ");
+ s.append(json.getString("error_code"));
+ if (json.has("error_string")) {
+ s.append(", string: ");
+ s.append(json.getString("error_string"));
+ }
+ myLog.l(Log.INFO, s.toString());
+
+ // Obsolete: there's no authentication anymore
+ // Dev code to enable frequent database wipes. If we fail to login,
+ // remove our stored account info, causing a create_account action
+ // next time.
+ // if(!Defaults.release) {
+ // if(json.getInt("error_code") == 11) {
+ // myLog.l(Log.DEBUG, "Dev: removing secret due to login failure");
+ // removeSecret();
+ // }
+ // }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Reads our persistent storage, looking for a stored proxy authentication secret.
+ *
+ * @return The secret, if present, or null.
+ */
+ // Obsolete, there's no authentication anymore
+ /*
+ * private String retrieveSecret() { SharedPreferences settings =
+ * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(),
+ * Defaults.getSettingsMode()); return settings.getString("proxySecret", null); }
+ */
+
+ // Obsolete, there's no authentication anymore
+ /*
+ * private void storeSecret(String secret) { SharedPreferences settings =
+ * Globals.getContext(). getSharedPreferences(Defaults.getSettingsName(),
+ * Defaults.getSettingsMode()); Editor editor = settings.edit();
+ * editor.putString("proxySecret", secret); editor.commit(); }
+ */
+
+ // Obsolete, there's no authentication anymore
+ /*
+ * private void removeSecret() { SharedPreferences settings = Globals.getContext().
+ * getSharedPreferences(Defaults.getSettingsName(), Defaults.getSettingsMode());
+ * Editor editor = settings.edit(); editor.remove("proxySecret"); editor.commit(); }
+ */
+
+ private void incomingCommand(JSONObject json) {
+ try {
+ String action = json.getString("action");
+ if (action.equals("control_connection_waiting")) {
+ startControlSession(json.getInt("port"));
+ } else if (action.equals("prefer_server")) {
+ String host = json.getString("host"); // throws JSONException, fine
+ preferServer(host);
+ myLog.i("New preferred server: " + host);
+ } else if (action.equals("message")) {
+ proxyMessage = json.getString("text");
+ myLog.i("Got news from proxy server: \"" + proxyMessage + "\"");
+ // TODO: send intent to notify UI about news
+ // FTPServerService.updateClients(); // UI update to show message
+ } else if (action.equals("noop")) {
+ myLog.d("Proxy noop");
+ } else {
+ myLog.l(Log.INFO, "Unsupported incoming action: " + action);
+ }
+ // If we're starting a control session register with ftpServerService
+ } catch (JSONException e) {
+ myLog.l(Log.INFO, "JSONException in proxy incomingCommand");
+ }
+ }
+
+ private void startControlSession(int port) {
+ Socket socket;
+ myLog.d("Starting new proxy FTP control session");
+ socket = newAuthedSocket(hostname, port);
+ if (socket == null) {
+ myLog.i("startControlSession got null authed socket");
+ return;
+ }
+ ProxyDataSocketFactory dataSocketFactory = new ProxyDataSocketFactory();
+ SessionThread thread = new SessionThread(socket, dataSocketFactory,
+ SessionThread.Source.PROXY);
+ thread.start();
+ ftpServerService.registerSessionThread(thread);
+ }
+
+ /**
+ * Connects an outgoing socket to the proxy and authenticates, creating an account if
+ * necessary.
+ */
+ private Socket newAuthedSocket(String hostname, int port) {
+ if (hostname == null) {
+ myLog.i("newAuthedSocket can't connect to null host");
+ return null;
+ }
+ JSONObject json = new JSONObject();
+ // String secret = retrieveSecret();
+ Socket socket;
+ OutputStream out = null;
+ InputStream in = null;
+
+ try {
+ myLog.d("Opening proxy connection to " + hostname + ":" + port);
+ socket = new Socket();
+ socket.connect(new InetSocketAddress(hostname, port), CONNECT_TIMEOUT);
+ json.put("android_id", Util.getAndroidId());
+ json.put("swiftp_version", Util.getVersion());
+ json.put("action", "login");
+ out = socket.getOutputStream();
+ in = socket.getInputStream();
+ int numBytes;
+
+ out.write(json.toString().getBytes(ENCODING));
+ myLog.l(Log.DEBUG, "Sent login request");
+ // Read and parse the server's response
+ byte[] bytes = new byte[IN_BUF_SIZE];
+ // Here we assume that the server's response will all be contained in
+ // a single read, which may be unsafe for large responses
+ numBytes = in.read(bytes);
+ if (numBytes == -1) {
+ myLog.l(Log.INFO, "Proxy socket closed while waiting for auth response");
+ return null;
+ } else if (numBytes == 0) {
+ myLog.l(Log.INFO, "Short network read waiting for auth, quitting");
+ return null;
+ }
+ json = new JSONObject(new String(bytes, 0, numBytes, ENCODING));
+ if (checkAndPrintJsonError(json)) {
+ return null;
+ }
+ myLog.d("newAuthedSocket successful");
+ return socket;
+ } catch (Exception e) {
+ myLog.i("Exception during proxy connection or authentication: " + e);
+ return null;
+ }
+ }
+
+ public void quit() {
+ setProxyState(State.DISCONNECTED);
+ try {
+ sendRequest(commandSocket, makeJsonRequest("finished")); // ignore reply
+
+ if (inputStream != null) {
+ myLog.d("quit() closing proxy inputStream");
+ inputStream.close();
+ } else {
+ myLog.d("quit() won't close null inputStream");
+ }
+ if (commandSocket != null) {
+ myLog.d("quit() closing proxy socket");
+ commandSocket.close();
+ } else {
+ myLog.d("quit() won't close null socket");
+ }
+ } catch (IOException e) {
+ } catch (JSONException e) {
+ }
+ persistProxyUsage();
+ Globals.setProxyConnector(null);
+ }
+
+ @SuppressWarnings("unused")
+ private JSONObject sendCmdSocketRequest(JSONObject json) {
+ try {
+ boolean queued;
+ synchronized (this) {
+ if (responseWaiter == null) {
+ responseWaiter = Thread.currentThread();
+ queued = false;
+ myLog.d("sendCmdSocketRequest proceeding without queue");
+ } else if (!responseWaiter.isAlive()) {
+ // This code should never run. It is meant to recover from a situation
+ // where there is a thread that sent a proxy request but died before
+ // starting the subsequent request. If this is the case, the correct
+ // behavior is to run the next queued thread in the queue, or if the
+ // queue is empty, to perform our own request.
+ myLog.l(Log.INFO, "Won't wait on dead responseWaiter");
+ if (queuedRequestThreads.size() == 0) {
+ responseWaiter = Thread.currentThread();
+ queued = false;
+ } else {
+ queuedRequestThreads.add(Thread.currentThread());
+ queuedRequestThreads.remove().interrupt(); // start queued thread
+ queued = true;
+ }
+ } else {
+ myLog.d("sendCmdSocketRequest queueing thread");
+ queuedRequestThreads.add(Thread.currentThread());
+ queued = true;
+ }
+ }
+ // If a different thread has sent a request and is waiting for a response,
+ // then the current thread will be in a queue waiting for an interrupt
+ if (queued) {
+ // The current thread must wait until we are popped off the waiting queue
+ // and receive an interrupt()
+ boolean interrupted = false;
+ try {
+ myLog.d("Queued cmd session request thread sleeping...");
+ Thread.sleep(QUEUE_WAIT_MS);
+ } catch (InterruptedException e) {
+ myLog.l(Log.DEBUG, "Proxy request popped and ready");
+ interrupted = true;
+ }
+ if (!interrupted) {
+ myLog.l(Log.INFO, "Timed out waiting on proxy queue");
+ return null;
+ }
+ }
+ // We have been popped from the wait queue if necessary, and now it's time
+ // to send the request.
+ try {
+ responseWaiter = Thread.currentThread();
+ byte[] outboundData = Util.jsonToByteArray(json);
+ try {
+ out.write(outboundData);
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "IOException sending proxy request");
+ return null;
+ }
+ // Wait RESPONSE_WAIT_MS for a response from the proxy
+ boolean interrupted = false;
+ try {
+ // Wait for the main ProxyConnector thread to interrupt us, meaning
+ // that a response has been received.
+ myLog.d("Cmd session request sleeping until response");
+ Thread.sleep(RESPONSE_WAIT_MS);
+ } catch (InterruptedException e) {
+ myLog.d("Cmd session response received");
+ interrupted = true;
+ }
+ if (!interrupted) {
+ myLog.l(Log.INFO, "Proxy request timed out");
+ return null;
+ }
+ // At this point, the main ProxyConnector thread will have stored
+ // our response in "JSONObject response".
+ myLog.d("Cmd session response was: " + response);
+ return response;
+ } finally {
+ // Make sure that when this request finishes, the next thread on the
+ // queue gets started.
+ synchronized (this) {
+ if (queuedRequestThreads.size() != 0) {
+ queuedRequestThreads.remove().interrupt();
+ }
+ }
+ }
+ } catch (JSONException e) {
+ myLog.l(Log.INFO, "JSONException in sendRequest: " + e);
+ return null;
+ }
+ }
+
+ public JSONObject sendRequest(InputStream in, OutputStream out, JSONObject request)
+ throws JSONException {
+ try {
+ out.write(Util.jsonToByteArray(request));
+ byte[] bytes = new byte[IN_BUF_SIZE];
+ int numBytes = in.read(bytes);
+ if (numBytes < 1) {
+ myLog.i("Proxy sendRequest short read on response");
+ return null;
+ }
+ JSONObject response = Util.byteArrayToJson(bytes);
+ if (response == null) {
+ myLog.i("Null response to sendRequest");
+ }
+ if (checkAndPrintJsonError(response)) {
+ myLog.i("Error response to sendRequest");
+ return null;
+ }
+ return response;
+ } catch (IOException e) {
+ myLog.i("IOException in proxy sendRequest: " + e);
+ return null;
+ }
+ }
+
+ public JSONObject sendRequest(Socket socket, JSONObject request) throws JSONException {
+ try {
+ if (socket == null) {
+ // The server is probably shutting down
+ myLog.i("null socket in ProxyConnector.sendRequest()");
+ return null;
+ } else {
+ return sendRequest(socket.getInputStream(), socket.getOutputStream(),
+ request);
+ }
+ } catch (IOException e) {
+ myLog.i("IOException in proxy sendRequest wrapper: " + e);
+ return null;
+ }
+ }
+
+ public ProxyDataSocketInfo pasvListen() {
+ try {
+ // connect to proxy and authenticate
+ myLog.d("Sending data_pasv_listen to proxy");
+ Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT);
+ if (socket == null) {
+ myLog.i("pasvListen got null socket");
+ return null;
+ }
+ JSONObject request = makeJsonRequest("data_pasv_listen");
+
+ JSONObject response = sendRequest(socket, request);
+ if (response == null) {
+ return null;
+ }
+ int port = response.getInt("port");
+ return new ProxyDataSocketInfo(socket, port);
+ } catch (JSONException e) {
+ myLog.l(Log.INFO, "JSONException in pasvListen");
+ return null;
+ }
+ }
+
+ public Socket dataPortConnect(InetAddress clientAddr, int clientPort) {
+ /**
+ * This function is called by a ProxyDataSocketFactory when it's time to transfer
+ * some data in PORT mode (not PASV mode). We send a data_port_connect request to
+ * the proxy, containing the IP and port of the FTP client to which a connection
+ * should be made.
+ */
+ try {
+ myLog.d("Sending data_port_connect to proxy");
+ Socket socket = newAuthedSocket(this.hostname, Defaults.REMOTE_PROXY_PORT);
+ if (socket == null) {
+ myLog.i("dataPortConnect got null socket");
+ return null;
+ }
+ JSONObject request = makeJsonRequest("data_port_connect");
+ request.put("address", clientAddr.getHostAddress());
+ request.put("port", clientPort);
+ JSONObject response = sendRequest(socket, request);
+ if (response == null) {
+ return null; // logged elsewhere
+ }
+ return socket;
+ } catch (JSONException e) {
+ myLog.i("JSONException in dataPortConnect");
+ return null;
+ }
+ }
+
+ /**
+ * Given a socket returned from pasvListen(), send a data_pasv_accept request over the
+ * socket to the proxy, which should result in a socket that is ready for data
+ * transfer with the FTP client. Of course, this will only work if the FTP client
+ * connects to the proxy like it's supposed to. The client will have already been told
+ * to connect by the response to its PASV command.
+ *
+ * This should only be called from the onTransfer method of ProxyDataSocketFactory.
+ *
+ * @param socket
+ * A socket previously returned from ProxyConnector.pasvListen()
+ * @return true if the accept operation completed OK, otherwise false
+ */
+
+ public boolean pasvAccept(Socket socket) {
+ try {
+ JSONObject request = makeJsonRequest("data_pasv_accept");
+ JSONObject response = sendRequest(socket, request);
+ if (response == null) {
+ return false; // error is logged elsewhere
+ }
+ if (checkAndPrintJsonError(response)) {
+ myLog.i("Error response to data_pasv_accept");
+ return false;
+ }
+ // The proxy's response will be an empty JSON object on success
+ myLog.d("Proxy data_pasv_accept successful");
+ return true;
+ } catch (JSONException e) {
+ myLog.i("JSONException in pasvAccept: " + e);
+ return false;
+ }
+ }
+
+ public InetAddress getProxyIp() {
+ if (this.isAlive()) {
+ if (commandSocket.isConnected()) {
+ return commandSocket.getInetAddress();
+ }
+ }
+ return null;
+ }
+
+ private JSONObject makeJsonRequest(String action) throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put("action", action);
+ return json;
+ }
+
+ /*
+ * Quotas have been canceled for now public QuotaStats getQuotaStats(boolean
+ * canUseCached) { if(canUseCached) { if(cachedQuotaStats != null) {
+ * myLog.d("Returning cachedQuotaStats"); return cachedQuotaStats; } else {
+ * myLog.d("Would return cached quota stats but none retrieved"); } } // If there's no
+ * cached quota stats, or if the called wants fresh stats, // make a JSON request to
+ * the proxy, assuming the command session is open. try { JSONObject response =
+ * sendCmdSocketRequest(makeJsonRequest("check_quota")); int used, quota; if(response
+ * == null) { myLog.w("check_quota got null response"); return null; } used =
+ * response.getInt("used"); quota = response.getInt("quota");
+ * myLog.d("Got quota response of " + used + "/" + quota); cachedQuotaStats = new
+ * QuotaStats(used, quota) ; return cachedQuotaStats; } catch (JSONException e) {
+ * myLog.w("JSONException in getQuota: " + e); return null; } }
+ */
+
+ // We want to track the total amount of data sent via the proxy server, to
+ // show it to the user and encourage them to donate.
+ void persistProxyUsage() {
+ if (proxyUsage == 0) {
+ return; // This shouldn't happen, but just for safety
+ }
+ SharedPreferences prefs = Globals.getContext().getSharedPreferences(
+ USAGE_PREFS_NAME, 0); // 0 == private
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putLong(USAGE_PREFS_NAME, proxyUsage);
+ editor.commit();
+ myLog.d("Persisted proxy usage to preferences");
+ }
+
+ long getPersistedProxyUsage() {
+ // This gets the last persisted value for bytes transferred through
+ // the proxy. It can be out of date since it doesn't include data
+ // transferred during the current session.
+ SharedPreferences prefs = Globals.getContext().getSharedPreferences(
+ USAGE_PREFS_NAME, 0); // 0 == private
+ return prefs.getLong(USAGE_PREFS_NAME, 0); // Default count of 0
+ }
+
+ long getProxyUsage() {
+ // This gets the running total of all proxy usage, which may not have
+ // been persisted yet.
+ return proxyUsage;
+ }
+
+ void incrementProxyUsage(long num) {
+ long oldProxyUsage = proxyUsage;
+ proxyUsage += num;
+ if (proxyUsage % UPDATE_USAGE_BYTES < oldProxyUsage % UPDATE_USAGE_BYTES) {
+ // TODO: Use intent to update UI
+ // FTPServerService.updateClients();
+ persistProxyUsage();
+ }
+ }
+
+ public State getProxyState() {
+ return proxyState;
+ }
+
+ private void setProxyState(State state) {
+ proxyState = state;
+ myLog.l(Log.DEBUG, "Proxy state changed to " + state, true);
+ // TODO: Use intent to update UI
+ // FTPServerService.updateClients();
+ }
+
+ static public String stateToString(State s) {
+ Context ctx = Globals.getContext();
+ switch (s) {
+ case DISCONNECTED:
+ return ctx.getString(R.string.pst_disconnected);
+ case CONNECTING:
+ return ctx.getString(R.string.pst_connecting);
+ case CONNECTED:
+ return ctx.getString(R.string.pst_connected);
+ case FAILED:
+ return ctx.getString(R.string.pst_failed);
+ case UNREACHABLE:
+ return ctx.getString(R.string.pst_unreachable);
+ default:
+ return ctx.getString(R.string.unknown);
+ }
+ }
+
+ /**
+ * The URL to which users should point their FTP client.
+ */
+ public String getURL() {
+ if (proxyState == State.CONNECTED) {
+ String username = Globals.getUsername();
+ if (username != null) {
+ return "ftp://" + prefix + "_" + username + "@" + hostname;
+ }
+ }
+ return Globals.getContext().getString(R.string.unknown);
+ }
+
+ /**
+ * If the proxy sends a human-readable message, it can be retrieved by calling this
+ * function. Returns null if no message has been received.
+ */
+ public String getProxyMessage() {
+ return proxyMessage;
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java b/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java
new file mode 100644
index 00000000..3864580e
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketFactory.java
@@ -0,0 +1,163 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.swiftp.Globals;
+
+import android.util.Log;
+
+/**
+ * @author david
+ *
+ */
+public class ProxyDataSocketFactory extends DataSocketFactory {
+ /**
+ * Implements data socket connections that go through our proxy server
+ * out on the net. The proxy sits between the FTP client and us, the server.
+ * We have to build in some coordination between the server and proxy in order
+ * for data sockets to be handled properly.
+ *
+ * When we receive a "PASV" command from a client, we have to request that the
+ * proxy server open a port, accept a connection, and proxy all data on that
+ * socket between ourself and the FTP client.
+ *
+ * When we receive a PORT command, we store the client's connection info,
+ * and when it's time to being transferring data, we request that the proxy
+ * make a connection to the client's IP & port and then proxy all data between
+ * ourself and the FTP client.
+ */
+
+ private Socket socket;
+ private int proxyListenPort;
+ ProxyConnector proxyConnector;
+ InetAddress clientAddress;
+ int clientPort;
+
+ public ProxyDataSocketFactory() {
+ clearState();
+ }
+
+ private void clearState() {
+ if(socket != null) {
+ try {
+ socket.close();
+ } catch (Exception e) {}
+ }
+ socket = null;
+ proxyConnector = null;
+ clientAddress = null;
+ proxyListenPort = 0;
+ clientPort = 0;
+ }
+
+ @Override
+ public InetAddress getPasvIp() {
+ ProxyConnector pc = Globals.getProxyConnector();
+ if(pc == null) {
+ return null;
+ }
+ return pc.getProxyIp();
+ }
+
+// public int getPortNumber() {
+// if(socket == )
+// return 0;
+// }
+
+ @Override
+ public int onPasv() {
+ clearState();
+ proxyConnector = Globals.getProxyConnector();
+ if(proxyConnector == null) {
+ myLog.l(Log.INFO, "Unexpected null proxyConnector in onPasv");
+ clearState();
+ return 0;
+ }
+ ProxyDataSocketInfo info = proxyConnector.pasvListen();
+ if(info == null) {
+ myLog.l(Log.INFO, "Null ProxyDataSocketInfo");
+ clearState();
+ return 0;
+ }
+ socket = info.getSocket();
+ proxyListenPort = info.getRemotePublicPort();
+ return proxyListenPort;
+ }
+
+ @Override
+ public boolean onPort(InetAddress dest, int port) {
+ clearState();
+ proxyConnector = Globals.getProxyConnector();
+ this.clientAddress = dest;
+ this.clientPort = port;
+ myLog.d("ProxyDataSocketFactory client port settings stored");
+ return true;
+ }
+
+ /**
+ * When the it's time for the SessionThread to actually begin PASV
+ * data transfer with the client, it will call this function to get
+ * a valid socket. The socket will have been created earlier with
+ * a call to onPasv(). The result of calling onTransfer() will be
+ * to cause the proxy to accept the incoming connection from the FTP
+ * client and start proxying back to us (the FTP server). The socket
+ * can then be handed back to the SessionThread which can use it as
+ * if it were directly connected to the client.
+ */
+ @Override
+ public Socket onTransfer() {
+ if(proxyConnector == null) {
+ myLog.w("Unexpected null proxyConnector in onTransfer");
+ return null;
+ }
+
+ if(socket == null) {
+ // We are in PORT mode (not PASV mode)
+ if(proxyConnector == null) {
+ myLog.l(Log.INFO, "Unexpected null proxyConnector in onTransfer");
+ return null;
+ }
+ // May return null, that's fine. ProxyConnector will log errors.
+ socket = proxyConnector.dataPortConnect(clientAddress, clientPort);
+ return socket;
+ } else {
+ // We are in PASV mode (not PORT mode)
+ if(proxyConnector.pasvAccept(socket)) {
+ return socket;
+ } else {
+ myLog.w("proxyConnector pasvAccept failed");
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public void reportTraffic(long bytes) {
+ ProxyConnector pc = Globals.getProxyConnector();
+ if(pc == null) {
+ myLog.d("Can't report traffic, null ProxyConnector");
+ } else {
+ pc.incrementProxyUsage(bytes);
+ }
+ }
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java b/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java
new file mode 100644
index 00000000..c1280c90
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/ProxyDataSocketInfo.java
@@ -0,0 +1,49 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.net.Socket;
+
+public class ProxyDataSocketInfo extends Socket {
+ private int remotePublicPort;
+ private Socket socket;
+
+ public Socket getSocket() {
+ return socket;
+ }
+
+ public void setSocket(Socket socket) {
+ this.socket = socket;
+ }
+
+ public ProxyDataSocketInfo(Socket socket, int remotePublicPort) {
+ this.remotePublicPort = remotePublicPort;
+ this.socket = socket;
+ }
+
+ public int getRemotePublicPort() {
+ return remotePublicPort;
+ }
+
+ public void setRemotePublicPort(int remotePublicPort) {
+ this.remotePublicPort = remotePublicPort;
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/SessionThread.java b/qftplib/src/main/java/org/swiftp/server/SessionThread.java
new file mode 100644
index 00000000..5fcb7336
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/SessionThread.java
@@ -0,0 +1,445 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+ */
+
+package org.swiftp.server;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.nio.ByteBuffer;
+
+import org.swiftp.Defaults;
+import org.swiftp.FTPServerService;
+import org.swiftp.Globals;
+import org.swiftp.MyLog;
+import org.swiftp.Util;
+
+import android.util.Log;
+
+public class SessionThread extends Thread {
+ protected boolean shouldExit = false;
+ protected Socket cmdSocket;
+ protected MyLog myLog = new MyLog(getClass().getName());
+ protected ByteBuffer buffer = ByteBuffer.allocate(Defaults
+ .getInputBufferSize());
+ protected boolean pasvMode = false;
+ protected boolean binaryMode = false;
+ protected Account account = new Account();
+ protected boolean authenticated = false;
+ protected File workingDir = Globals.getChrootDir();
+ // protected ServerSocket dataServerSocket = null;
+ protected Socket dataSocket = null;
+ // protected FTPServerService service;
+ protected File renameFrom = null;
+ // protected InetAddress outDataDest = null;
+ // protected int outDataPort = 20; // 20 is the default ftp-data port
+ protected DataSocketFactory dataSocketFactory;
+ OutputStream dataOutputStream = null;
+ private boolean sendWelcomeBanner;
+ protected String encoding = Defaults.SESSION_ENCODING;
+ protected Source source;
+ int authFails = 0;
+
+ public enum Source {LOCAL, PROXY}; // where did this connection come from?
+ public static int MAX_AUTH_FAILS = 3;
+ /**
+ * Used when we get a PORT command to open up an outgoing socket.
+ *
+ * @return
+ */
+ // public void setPortSocket(InetAddress dest, int port) {
+ // myLog.l(Log.DEBUG, "Setting PORT dest to " +
+ // dest.getHostAddress() + " port " + port);
+ // outDataDest = dest;
+ // outDataPort = port;
+ // }
+ /**
+ * Sends a string over the already-established data socket
+ *
+ * @param string
+ * @return Whether the send completed successfully
+ */
+ public boolean sendViaDataSocket(String string) {
+ try {
+ byte[] bytes = string.getBytes(encoding);
+ myLog.d("Using data connection encoding: " + encoding);
+ return sendViaDataSocket(bytes, bytes.length);
+ } catch (UnsupportedEncodingException e) {
+ myLog.l(Log.ERROR, "Unsupported encoding for data socket send");
+ return false;
+ }
+ }
+
+ public boolean sendViaDataSocket(byte[] bytes, int len) {
+ return sendViaDataSocket(bytes, 0, len);
+ }
+
+ /**
+ * Sends a byte array over the already-established data socket
+ *
+ * @param bytes
+ * @param len
+ * @return
+ */
+ public boolean sendViaDataSocket(byte[] bytes, int start, int len) {
+
+ if (dataOutputStream == null) {
+ myLog.l(Log.INFO, "Can't send via null dataOutputStream");
+ return false;
+ }
+ if (len == 0) {
+ return true; // this isn't an "error"
+ }
+ try {
+ dataOutputStream.write(bytes, start, len);
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "Couldn't write output stream for data socket");
+ myLog.l(Log.INFO, e.toString());
+ return false;
+ }
+ dataSocketFactory.reportTraffic(len);
+ return true;
+ }
+
+ /**
+ * Received some bytes from the data socket, which is assumed to already be
+ * connected. The bytes are placed in the given array, and the number of
+ * bytes successfully read is returned.
+ *
+ * @param bytes
+ * Where to place the input bytes
+ * @return >0 if successful which is the number of bytes read, -1 if no
+ * bytes remain to be read, -2 if the data socket was not connected,
+ * 0 if there was a read error
+ */
+ public int receiveFromDataSocket(byte[] buf) {
+ int bytesRead;
+
+ if (dataSocket == null) {
+ myLog.l(Log.INFO, "Can't receive from null dataSocket");
+ return -2;
+ }
+ if (!dataSocket.isConnected()) {
+ myLog.l(Log.INFO, "Can't receive from unconnected socket");
+ return -2;
+ }
+ InputStream in;
+ try {
+ in = dataSocket.getInputStream();
+ // If the read returns 0 bytes, the stream is not yet
+ // closed, but we just want to read again.
+ while ((bytesRead = in.read(buf, 0, buf.length)) == 0) {
+ }
+ if (bytesRead == -1) {
+ // If InputStream.read returns -1, there are no bytes
+ // remaining, so we return 0.
+ return -1;
+ }
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "Error reading data socket");
+ return 0;
+ }
+ dataSocketFactory.reportTraffic(bytesRead);
+ return bytesRead;
+ }
+
+ /**
+ * Called when we receive a PASV command.
+ *
+ * @return Whether the necessary initialization was successful.
+ */
+ public int onPasv() {
+ return dataSocketFactory.onPasv();
+ }
+
+ /**
+ * Called when we receive a PORT command.
+ *
+ * @return Whether the necessary initialization was successful.
+ */
+ public boolean onPort(InetAddress dest, int port) {
+ return dataSocketFactory.onPort(dest, port);
+ }
+
+ public InetAddress getDataSocketPasvIp() {
+ // When the client sends PASV, our reply will contain the address and port
+ // of the data connection that the client should connect to. For this purpose
+ // we always use the same IP address that the command socket is using.
+ return cmdSocket.getLocalAddress();
+
+ // The old code, not totally correct.
+ // return dataSocketFactory.getPasvIp();
+ }
+
+ // public int getDataSocketPort() {
+ // return dataSocketFactory.getPortNumber();
+ // }
+
+ /**
+ * Will be called by (e.g.) CmdSTOR, CmdRETR, CmdLIST, etc. when they are
+ * about to start actually doing IO over the data socket.
+ *
+ * @return
+ */
+ public boolean startUsingDataSocket() {
+ try {
+ dataSocket = dataSocketFactory.onTransfer();
+ if (dataSocket == null) {
+ myLog.l(Log.INFO,
+ "dataSocketFactory.onTransfer() returned null");
+ return false;
+ }
+ dataOutputStream = dataSocket.getOutputStream();
+ return true;
+ } catch (IOException e) {
+ myLog.l(Log.INFO,
+ "IOException getting OutputStream for data socket");
+ dataSocket = null;
+ return false;
+ }
+ }
+
+ public void quit() {
+ myLog.d("SessionThread told to quit");
+ closeSocket();
+ }
+
+ public void closeDataSocket() {
+ myLog.l(Log.DEBUG, "Closing data socket");
+ if (dataOutputStream != null) {
+ try {
+ dataOutputStream.close();
+ } catch (IOException e) {
+ }
+ dataOutputStream = null;
+ }
+ if (dataSocket != null) {
+ try {
+ dataSocket.close();
+ } catch (IOException e) {
+ }
+ }
+ dataSocket = null;
+ }
+
+ protected InetAddress getLocalAddress() {
+ return cmdSocket.getLocalAddress();
+ }
+
+ static int numNulls = 0;
+ @Override
+ public void run() {
+ myLog.l(Log.INFO, "SessionThread started");
+
+ if(sendWelcomeBanner) {
+ writeString("220 SwiFTP " + Util.getVersion() + " ready\r\n");
+ }
+ // Main loop: read an incoming line and process it
+ try {
+ BufferedReader in = new BufferedReader(new InputStreamReader(cmdSocket
+ .getInputStream()), 8192); // use 8k buffer
+ while (true) {
+ String line;
+ line = in.readLine(); // will accept \r\n or \n for terminator
+ if (line != null) {
+ FTPServerService.writeMonitor(true, line);
+ myLog.l(Log.DEBUG, "Received line from client: " + line);
+ FtpCmd.dispatchCommand(this, line);
+ } else {
+ myLog.i("readLine gave null, quitting");
+ break;
+ }
+ }
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "Connection was dropped");
+ }
+ closeSocket();
+ }
+
+ /**
+ * A static method to check the equality of two byte arrays, but only up to
+ * a given length.
+ */
+ public static boolean compareLen(byte[] array1, byte[] array2, int len) {
+ for (int i = 0; i < len; i++) {
+ if (array1[i] != array2[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void closeSocket() {
+ if (cmdSocket == null) {
+ return;
+ }
+ try {
+ cmdSocket.close();
+ } catch (IOException e) {}
+ }
+
+ public void writeBytes(byte[] bytes) {
+ try {
+ // TODO: do we really want to do all of this on each write? Why?
+ BufferedOutputStream out = new BufferedOutputStream(cmdSocket
+ .getOutputStream(), Defaults.dataChunkSize);
+ out.write(bytes);
+ out.flush();
+ dataSocketFactory.reportTraffic(bytes.length);
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "Exception writing socket");
+ closeSocket();
+ return;
+ }
+ }
+
+ public void writeString(String str) {
+ FTPServerService.writeMonitor(false, str);
+ byte[] strBytes;
+ try {
+ strBytes = str.getBytes(encoding);
+ } catch (UnsupportedEncodingException e) {
+ myLog.e("Unsupported encoding: " + encoding);
+ strBytes = str.getBytes();
+ }
+ writeBytes(strBytes);
+ }
+
+ protected Socket getSocket() {
+ return cmdSocket;
+ }
+
+ public Account getAccount() {
+ return account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ }
+
+ public boolean isPasvMode() {
+ return pasvMode;
+ }
+
+ public SessionThread(Socket socket, DataSocketFactory dataSocketFactory,
+ Source source) {
+ this.cmdSocket = socket;
+ this.source = source;
+ this.dataSocketFactory = dataSocketFactory;
+ if(source == Source.LOCAL) {
+ this.sendWelcomeBanner = true;
+ } else {
+ this.sendWelcomeBanner = false;
+ }
+ }
+
+ static public ByteBuffer stringToBB(String s) {
+ return ByteBuffer.wrap(s.getBytes());
+ }
+
+ public boolean isBinaryMode() {
+ return binaryMode;
+ }
+
+ public void setBinaryMode(boolean binaryMode) {
+ this.binaryMode = binaryMode;
+ }
+
+ public boolean isAuthenticated() {
+ return authenticated;
+ }
+
+ public void authAttempt(boolean authenticated) {
+ if (authenticated) {
+ myLog.l(Log.INFO, "Authentication complete");
+ this.authenticated = true;
+ } else {
+ // There was a failed auth attempt. If the connection came
+ // via the proxy, then drop it now. The client can't try again
+ // successfully because it doesn't know its real username. What
+ // it knows is prefix_username.
+ if(source == Source.PROXY) {
+ quit();
+ } else {
+ authFails++;
+ myLog.i("Auth failed: " + authFails + "/" + MAX_AUTH_FAILS);
+ }
+ if(authFails > MAX_AUTH_FAILS) {
+ myLog.i("Too many auth fails, quitting session");
+ quit();
+ }
+ }
+
+ }
+
+ public File getWorkingDir() {
+ return workingDir;
+ }
+
+ public void setWorkingDir(File workingDir) {
+ try {
+ this.workingDir = workingDir.getCanonicalFile().getAbsoluteFile();
+ } catch (IOException e) {
+ myLog.l(Log.INFO, "SessionThread canonical error");
+ }
+ }
+
+ /*
+ * public FTPServerService getService() { return service; }
+ *
+ * public void setService(FTPServerService service) { this.service =
+ * service; }
+ */
+
+ public Socket getDataSocket() {
+ return dataSocket;
+ }
+
+ public void setDataSocket(Socket dataSocket) {
+ this.dataSocket = dataSocket;
+ }
+
+ // public ServerSocket getServerSocket() {
+ // return dataServerSocket;
+ // }
+
+ public File getRenameFrom() {
+ return renameFrom;
+ }
+
+ public void setRenameFrom(File renameFrom) {
+ this.renameFrom = renameFrom;
+ }
+
+ public String getEncoding() {
+ return encoding;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+}
diff --git a/qftplib/src/main/java/org/swiftp/server/TcpListener.java b/qftplib/src/main/java/org/swiftp/server/TcpListener.java
new file mode 100644
index 00000000..7f199104
--- /dev/null
+++ b/qftplib/src/main/java/org/swiftp/server/TcpListener.java
@@ -0,0 +1,67 @@
+/*
+Copyright 2009 David Revell
+
+This file is part of SwiFTP.
+
+SwiFTP 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.
+
+SwiFTP 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 SwiFTP. If not, see .
+*/
+
+package org.swiftp.server;
+
+import java.net.ServerSocket;
+import java.net.Socket;
+
+import org.swiftp.FTPServerService;
+import org.swiftp.MyLog;
+
+import android.util.Log;
+
+public class TcpListener extends Thread {
+ ServerSocket listenSocket;
+ FTPServerService ftpServerService;
+ MyLog myLog = new MyLog(getClass().getName());
+
+ public TcpListener(ServerSocket listenSocket, FTPServerService ftpServerService) {
+ this.listenSocket = listenSocket;
+ this.ftpServerService = ftpServerService;
+ }
+
+ public void quit() {
+ try {
+ listenSocket.close(); // if the TcpListener thread is blocked on accept,
+ // closing the socket will raise an exception
+ } catch (Exception e) {
+ myLog.l(Log.DEBUG, "Exception closing TcpListener listenSocket");
+ }
+ }
+
+ @Override
+ public void run() {
+ try {
+ while(true) {
+
+ Socket clientSocket = listenSocket.accept();
+ myLog.l(Log.INFO, "New connection, spawned thread");
+ SessionThread newSession = new SessionThread(clientSocket,
+ new NormalDataSocketFactory(),
+ SessionThread.Source.LOCAL);
+ newSession.start();
+ ftpServerService.registerSessionThread(newSession);
+ }
+ } catch (Exception e) {
+ myLog.l(Log.DEBUG, "Exception in TcpListener");
+ }
+ }
+}
+
diff --git a/qftplib/src/main/java/util/DocumentUtil.java b/qftplib/src/main/java/util/DocumentUtil.java
new file mode 100644
index 00000000..2f57e4da
--- /dev/null
+++ b/qftplib/src/main/java/util/DocumentUtil.java
@@ -0,0 +1,517 @@
+package util;
+//by 乘着船 at 2021-2023
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageVolume;
+import android.preference.PreferenceManager;
+import android.support.v4.provider.DocumentFile;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class DocumentUtil {
+
+ private static final String TAG = DocumentUtil.class.getSimpleName();
+
+ public static final int OPEN_DOCUMENT_TREE_CODE = 8000;
+ public static final int MAX_BUFFER_SIZE = 5242880;//max buffer size 5MB
+
+ public static final String SDCARD = Environment.getExternalStorageDirectory().getAbsolutePath();
+ public static final String SDCARD_PATH = SDCARD + "/";
+ public static final String CONTENT_PRF = "content://";
+ public static final String[] SDCARD_CONTENT = new String[]{
+ CONTENT_PRF + "com.android.externalstorage.documents/tree/primary%3A", "/document/primary%3A"};
+ public static final String ANDROID_PATH = SDCARD_PATH + "Android/";
+ public static final String[] ANDROID_CONTENT = new String[]{
+ SDCARD_CONTENT[0] + "Android%2F", SDCARD_CONTENT[1] + "Android%2F"};
+
+ public static final int ANDROID_SAVE_INTENT =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION |
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION ;
+ public static final int ANDROID_OPEN_INTENT =
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
+ Intent.FLAG_GRANT_PREFIX_URI_PERMISSION |
+ ANDROID_SAVE_INTENT ;
+
+ public static List sExtSdCardPaths = new ArrayList<>();
+
+ private static String requestRootPath = null;
+
+ private DocumentUtil() {
+
+ }
+
+ public static void cleanCache() {
+ sExtSdCardPaths.clear();
+ }
+
+ /**
+ * Get a list of external SD card paths. (Kitkat or higher.)
+ *
+ * @return A list of external SD card paths.
+ */
+ private static String[] getExtSdCardPaths(Context context) {
+ if (sExtSdCardPaths.size() > 0) {
+ return sExtSdCardPaths.toArray(new String[0]);
+ }
+ for (File file : context.getExternalFilesDirs("external")) {
+ if (file != null && !file.equals(context.getExternalFilesDir("external"))) {
+ int index = file.getAbsolutePath().indexOf("/Android/data");
+ if (index < 0) {
+ Log.d(TAG, "Unexpected external file dir: " + file.getAbsolutePath());
+ } else {
+ String path = file.getAbsolutePath().substring(0, index);
+ try {
+ path = new File(path).getCanonicalPath();
+ } catch (IOException e) {
+ // Keep non-canonical path.
+ }
+ sExtSdCardPaths.add(path);
+ }
+ }
+ }
+ if (sExtSdCardPaths.isEmpty()) return new String[0];//{SDCARD};
+ return sExtSdCardPaths.toArray(new String[0]);
+ }
+
+ /**
+ * Determine the main folder of the external SD card containing the given file.
+ *
+ * @param file the file.
+ * @return The main folder of the external SD card containing this file, if the file is on an SD
+ * card. Otherwise,
+ * null is returned.
+ */
+ private static String getExtSdCardFolder(final File file, Context context) {
+ String[] extSdPaths = getExtSdCardPaths(context);
+ try {
+ for (String extSdPath : extSdPaths) {
+ if (file.getCanonicalPath().startsWith(extSdPath)) {
+ return extSdPath;
+ }
+ }
+ } catch (IOException e) {
+ return null;
+ }
+ return null;
+ }
+
+ /**
+ * Determine if a file is on external sd card. (Kitkat or higher.)
+ *
+ * @param file The file.
+ * @return true if on external sd card.
+ */
+ public static boolean isOnExtSdCard(final File file, Context c) {
+ return getExtSdCardFolder(file, c) != null;
+ }
+
+ /**
+ * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5).
+ * If the file is not
+ * existing, it is created.
+ *
+ * @param file The file.
+ * @param isDirectory true/false/null
+ * true/false - flag indicating if the file should be a directory ,
+ * if file not exist, it will be create .
+ * null - do not know the file is a directory ,
+ * if file not exist, it will not be create .
+ * @return The DocumentFile
+ */
+ public static DocumentFile getDocumentFile(
+ final File file, final Boolean isDirectory, Context context) {
+
+ String baseFolder = getExtSdCardFolder(file, context);
+ boolean originalDirectory = false;
+ if (baseFolder == null) {
+ return null;
+ }
+
+ String relativePath = null;
+ try {
+ String fullPath = file.getCanonicalPath();
+ if (!baseFolder.equals(fullPath)) {
+ relativePath = fullPath.substring(baseFolder.length() + 1);
+ } else {
+ originalDirectory = true;
+ }
+ } catch (IOException e) {
+ return null;
+ } catch (Exception f) {
+ originalDirectory = true;
+ //continue
+ }
+ String as = PreferenceManager.getDefaultSharedPreferences(context).getString(baseFolder,
+ null);
+
+ Uri treeUri = null;
+ if (as != null) treeUri = Uri.parse(as);
+ if (treeUri == null) {
+ return null;
+ }
+
+ // start with root of SD card and then parse through document tree.
+ DocumentFile document = DocumentFile.fromTreeUri(context, treeUri);
+ if (originalDirectory) return document;
+ String[] parts = relativePath.split("/");
+ for (int i = 0; i < parts.length; i++) {
+ DocumentFile nextDocument = document.findFile(parts[i]);
+
+ if (nextDocument == null) {
+ if(isDirectory == null) return null;
+ if ((i < parts.length - 1) || isDirectory) {
+ nextDocument = document.createDirectory(parts[i]);
+ } else {
+ nextDocument = document.createFile("image", parts[i]);
+ }
+ }
+ document = nextDocument;
+ }
+
+ return document;
+ }
+
+ public static boolean mkdirs(Context context, File dir) {
+ boolean res = dir.mkdirs();
+ if (!res) {
+ if (isOnExtSdCard(dir, context)) {
+ DocumentFile documentFile = getDocumentFile(dir, true, context);
+ res = documentFile != null && documentFile.canWrite();
+ }
+ }
+ return res;
+ }
+
+ private static boolean FileDelete(File file) {
+ if (file.isFile()) return file.delete();
+ File[] subFiles = file.listFiles();
+ if (subFiles==null || subFiles.length==0)
+ return file.delete();
+ boolean ret = true;
+ for(File subFile:subFiles){
+ if (subFile.isDirectory()) ret = FileDelete(subFile) && ret;
+ else ret = subFile.delete() && ret;
+ }
+ return ret && file.delete();
+ }
+
+ public static boolean delete(Context context, File file) {
+ boolean ret = FileDelete(file);
+ if (ret) return ret;
+ //if (isOnExtSdCard(file, context)) {
+ DocumentFile f = getDocumentFile(file, false, context);
+ if (f != null) {
+ ret = f.delete();
+ }
+ //}
+ return ret;
+ }
+
+ public static boolean canWrite(File file) {
+ boolean res = file.exists() && file.canWrite();
+
+ if (!res && !file.exists()) {
+ try {
+ if (!file.isDirectory()) {
+ res = file.createNewFile() && file.delete();
+ } else {
+ res = file.mkdirs() && file.delete();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return res;
+ }
+
+ /*public static boolean canWrite(Context context, File file) {
+ boolean res = canWrite(file);
+
+ if (!res && isOnExtSdCard(file, context)) {
+ DocumentFile documentFile = getDocumentFile(file, true, context);
+ res = documentFile != null && documentFile.canWrite();
+ }
+ return res;
+ }*/
+
+ private static boolean renameToCross(Context context,File src,File dest) throws Exception {
+ copy(context,src,dest);
+ boolean exist = dest.exists();
+ if (!exist) {
+ DocumentFile Dest = getDocumentFile(dest,null,context);
+ if(Dest!=null && Dest.exists())
+ exist = true;
+ }
+ delete(context,src);
+ return exist;
+ }
+
+ public static boolean renameTo(Context context, File src, File dest) throws Exception {
+ boolean res = src.renameTo(dest);
+ if (res) return true;
+
+ if (isOnExtSdCard(dest, context)) {
+ DocumentFile srcDoc;
+ if (isOnExtSdCard(src, context)) {
+ srcDoc = getDocumentFile(src, false, context);
+ } else {
+ srcDoc = DocumentFile.fromFile(src);
+ }
+ DocumentFile destDoc = getDocumentFile(dest.getParentFile(), true, context);
+
+ if (srcDoc != null && destDoc != null) {
+ if(!srcDoc.exists())
+ throw new FileNotFoundException(src.toString());
+ if (Objects.equals(src.getParent(), dest.getParent())) {
+ DocumentFile DestDoc = getDocumentFile(dest, null, context);
+ if(DestDoc!=null && DestDoc.exists()){
+ if(dest.isDirectory())
+ return renameToCross(context,src,dest);
+ else if(!DestDoc.delete())
+ return false;
+ }
+ res = srcDoc.renameTo(dest.getName());
+ } else return renameToCross(context, src, dest);
+ } else {
+ if ((src.exists() || srcDoc!=null) && (dest.canWrite() || destDoc!=null))
+ return renameToCross(context, src, dest);
+ else return false;
+ }
+ } else return renameToCross(context, src, dest);
+
+ return res;
+ }
+
+ public static InputStream getInputStream(Context context, File destFile) {
+ InputStream in = null;
+ try {
+ if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {
+ DocumentFile file = getDocumentFile(destFile, false, context);
+ if (file != null && file.canWrite()) {
+ in = context.getContentResolver().openInputStream(file.getUri());
+ }
+ } else {
+ in = new FileInputStream(destFile);
+
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ return in;
+ }
+
+ public static OutputStream getOutputStream(Context context, File destFile, boolean append) {
+ OutputStream out = null;
+ try {
+ if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {
+ DocumentFile file = getDocumentFile(destFile, false, context);
+ if (file != null && file.canWrite()) {
+ String mode;
+ if(append)
+ mode = "wa";
+ else mode = "wt";
+ out = context.getContentResolver().openOutputStream(file.getUri(), mode);
+ }
+ } else {
+ out = new FileOutputStream(destFile, append);
+
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ return out;
+ }
+
+ public static OutputStream getOutputStream(Context context, File destFile) {
+ OutputStream out = null;
+ try {
+ if (!canWrite(destFile) && isOnExtSdCard(destFile, context)) {
+ DocumentFile file = getDocumentFile(destFile, false, context);
+ if (file != null && file.canWrite())
+ out = context.getContentResolver().openOutputStream(file.getUri(),"wt");
+ } else {
+ out = new FileOutputStream(destFile);
+
+ }
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ }
+ return out;
+ }
+
+ public static void saveTreeUri(Context context, String rootPath, Uri uri) {
+ DocumentFile file = DocumentFile.fromTreeUri(context, uri);
+ if (file != null && file.canWrite()) {
+ SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context);
+ perf.edit().putString(rootPath, uri.toString()).apply();
+ } else {
+ Log.e(TAG, "no write permission: " + rootPath);
+ }
+ }
+
+ public static boolean checkWritableRootPath(Context context, String rootPath) {
+ File root = new File(rootPath);
+ if (!root.canWrite()) {
+
+ if (isOnExtSdCard(root, context)) {
+ DocumentFile documentFile = getDocumentFile(root, true, context);
+ return documentFile == null || !documentFile.canWrite();
+ } else {
+ SharedPreferences perf = PreferenceManager.getDefaultSharedPreferences(context);
+
+ String documentUri = perf.getString(rootPath, "");
+
+ if (documentUri == null || documentUri.isEmpty()) {
+ return true;
+ } else {
+ DocumentFile file = DocumentFile.fromTreeUri(context, Uri.parse(documentUri));
+ return !(file != null && file.canWrite());
+ }
+ }
+ }
+ return false;
+ }
+
+ private static void copyFile (
+ Context context, File srcFile, File destFile) throws Exception {
+ InputStream fis=getInputStream(context,srcFile);
+ if (fis==null) fis=new FileInputStream(srcFile);
+ OutputStream fos=getOutputStream(context,destFile);
+ if (fos==null) fos=new FileOutputStream(destFile);
+ int len = fis.available();
+ if (len>MAX_BUFFER_SIZE) len=MAX_BUFFER_SIZE;
+ byte[] flush =new byte [len];
+ while((len=fis.read(flush))>0) {
+ fos.write(flush,0,len);
+ }
+ fos.flush();
+ fis.close();
+ fos.close();
+ }
+
+ private static void copyTree (
+ Context context,File srcFolder,File destFolder)
+ throws Exception{
+ DocumentFile DestFolder=getDocumentFile(destFolder,true,context);
+ if (DestFolder==null) destFolder.mkdirs();
+ DocumentFile SrcFolder=getDocumentFile(srcFolder,true,context);
+ String name;
+ File srcSub,destSub;
+ if (SrcFolder != null) {
+ DocumentFile[] SrcSubs = SrcFolder.listFiles();
+ for(DocumentFile SrcSub:SrcSubs) {
+ name = SrcSub.getName();
+ srcSub=new File(srcFolder.getAbsolutePath(), name);
+ destSub=new File(destFolder.getAbsolutePath(), name);
+ if (SrcSub.isDirectory()){
+ copyTree(context,srcSub,destSub);
+ } else {
+ copyFile(context,srcSub,destSub);
+ }
+ }
+ } else {
+ File[] SrcSubs = srcFolder.listFiles();
+ for(File SrcSub:SrcSubs) {
+ name = SrcSub.getName();
+ srcSub=new File(srcFolder.getAbsolutePath(), name);
+ destSub=new File(destFolder.getAbsolutePath(), name);
+ if (SrcSub.isDirectory()){
+ copyTree(context,srcSub,destSub);
+ } else {
+ copyFile(context,srcSub,destSub);
+ }
+ }
+ }
+ }
+
+ public static void copy (
+ Context context, File src, File dest)
+ throws Exception{
+ if (isDirectory(context,src))
+ copyTree(context,src,dest);
+ else copyFile(context,src,dest);
+ }
+
+ public static String[] listFiles (
+ Context context,File folder)
+ throws Exception{
+ DocumentFile Folder=getDocumentFile(folder,true,context);
+ if (Folder == null) {
+ File[] Subs = folder.listFiles();
+ if (Subs==null) return null;
+ String[] subs=new String[Subs.length];
+ for (int i=0;i fileUtils = Class.forName("android.os.FileUtils");
+ Method setPermissions =
+ fileUtils.getMethod("setPermissions", String.class, int.class, int.class, int.class);
+ return (Integer) setPermissions.invoke(null, path.getAbsolutePath(), mode, -1, -1);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ public static void setPermission(File file) throws IOException {
+ Set perms = new HashSet<>();
+ perms.add(PosixFilePermission.OWNER_READ);
+ perms.add(PosixFilePermission.OWNER_WRITE);
+ perms.add(PosixFilePermission.OWNER_EXECUTE);
+
+ perms.add(PosixFilePermission.OTHERS_READ);
+ perms.add(PosixFilePermission.OTHERS_WRITE);
+ perms.add(PosixFilePermission.OWNER_EXECUTE);
+
+ perms.add(PosixFilePermission.GROUP_READ);
+ perms.add(PosixFilePermission.GROUP_WRITE);
+ perms.add(PosixFilePermission.GROUP_EXECUTE);
+
+ Files.setPosixFilePermissions(file.toPath(), perms);
+ }
+
+ public static boolean recursiveChmod(File root, int mode) throws Exception {
+ boolean success = chmod(root, mode) == 0;
+ for (File path : root.listFiles()) {
+ if (path.isDirectory()) {
+ success = recursiveChmod(path, mode);
+ }
+ success &= (chmod(path, mode) == 0);
+ }
+ return success;
+ }
+
+ public static boolean delete(String path) {
+ return delete(new File(path));
+ }
+
+ public static boolean delete(File path) {
+ boolean result = true;
+ if (path.canWrite()) {
+ if (path.isDirectory()) {
+ for (File child : path.listFiles()) {
+ result &= delete(child);
+ }
+ result &= path.delete(); // Delete empty directory.
+ } else if (path.isFile()) {
+ result = path.delete();
+ }
+ //if (!result) {
+ //Log.e(TAG, "Delete failed;");
+ //}
+ return result;
+ } else {
+ if (path.exists()) {
+ DocumentFile documentFile = DocumentUtil.getDocumentFile(path,null,activity);
+ if(documentFile!=null)
+ return documentFile.delete();
+ else return false;
+ } else {
+ //Log.e(TAG, "File does not exist.");
+ return false;
+ }
+ }
+ }
+
+ public static File copyFromStream(String name, InputStream input) {
+ if (name == null || name.length() == 0) {
+ //Log.e(TAG, "No script name specified.");
+ return null;
+ }
+ File file = new File(name);
+ if (!makeDirectories(file.getParentFile(), 0755)) {
+ return null;
+ }
+ try {
+ OutputStream output = new FileOutputStream(file);
+ IoUtil.copy(input, output);
+ } catch (Exception e) {
+ //Log.e(TAG, e);
+ return null;
+ }
+ return file;
+ }
+
+ public static boolean makeDirectories(File directory, int mode) {
+ File parent = directory;
+ while (parent.getParentFile() != null && !parent.exists()) {
+ parent = parent.getParentFile();
+ }
+ if (!directory.exists()) {
+ //Log.d(TAG, "Creating directory: " + directory.getName());
+ if (!directory.mkdirs()) {
+ //Log.e(TAG, "Failed to create directory.");
+ return false;
+ }
+ }
+ try {
+ recursiveChmod(parent, mode);
+ } catch (Exception e) {
+ //Log.e(TAG, e);
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean mkdir(File directory) {
+ boolean result;
+ if (directory.exists())
+ return false;
+ result = directory.mkdirs();
+ if(!result) {
+ DocumentFile documentFile = DocumentUtil.getDocumentFile(directory, true, activity);
+ result = documentFile != null && documentFile.exists();
+ }
+ return result;
+ }
+
+ public static File getExternalDownload() {
+ try {
+ Class> c = Class.forName("android.os.Environment");
+ Method m = c.getDeclaredMethod("getExternalStoragePublicDirectory", String.class);
+ String download = c.getDeclaredField("DIRECTORY_DOWNLOADS").get(null).toString();
+ return (File) m.invoke(null, download);
+ } catch (Exception e) {
+ return new File(Environment.getExternalStorageDirectory(), "Download");
+ }
+ }
+
+ public static boolean rename(String oldPath, String newPath) {
+ return rename(new File(oldPath),new File(newPath));
+ }
+
+ public static boolean rename(File file, String name) {
+ return rename(file,new File(file.getParent(), name));
+ }
+ public static boolean rename(File oldFile,File newFile) {
+ boolean result =oldFile.renameTo(newFile);
+ if(!result && oldFile.exists()) {
+ try {
+ DocumentFile documentFile = DocumentUtil.getDocumentFile(oldFile,null,activity);
+ if (documentFile != null)
+ result = documentFile.renameTo(newFile.getName());
+ } catch (Exception exception) {
+ exception.printStackTrace();
+ }
+ }
+ return result;
+ }
+
+ public static String readToString(File file) throws IOException {
+ if (file == null || !file.exists()) {
+ return null;
+ }
+ FileReader reader = new FileReader(file);
+ StringBuilder out = new StringBuilder();
+ char[] buffer = new char[1024 * 4];
+ int numRead = 0;
+ while ((numRead = reader.read(buffer)) > -1) {
+ out.append(String.valueOf(buffer, 0, numRead));
+ }
+ reader.close();
+ return out.toString();
+ }
+
+ public static String readFromAssetsFile(Context context, String name) throws IOException {
+ AssetManager am = context.getAssets();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(am.open(name)));
+ String line;
+ StringBuilder builder = new StringBuilder();
+ while ((line = reader.readLine()) != null) {
+ builder.append(line);
+ }
+ reader.close();
+ return builder.toString();
+ }
+
+ public static void lnOrcopy(File src, File dst, int sdk) throws IOException, ErrnoException {
+
+ if (sdk>=21) {
+ Os.symlink(src.getAbsolutePath(), dst.getAbsolutePath());
+ } else {
+ FileInputStream inStream = new FileInputStream(src);
+ FileOutputStream outStream = new FileOutputStream(dst);
+ FileChannel inChannel = inStream.getChannel();
+ FileChannel outChannel = outStream.getChannel();
+ inChannel.transferTo(0, inChannel.size(), outChannel);
+ inStream.close();
+ outStream.close();
+ }
+ }
+
+ public static String getFileContents(String filename) {
+
+ File scriptFile = new File( filename );
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ while ((line = in.readLine())!=null) {
+ tContent.append(line).append("\n");
+ }
+ in.close();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ return tContent.toString();
+ }
+
+ public static String getFileContents(String filename, int pos) {
+
+ File scriptFile = new File( filename );
+ StringBuilder tContent = new StringBuilder();
+ if (scriptFile.exists()) {
+ BufferedReader in;
+ try {
+ in = new BufferedReader(new FileReader(scriptFile));
+ String line;
+
+ while ((line = in.readLine())!=null) {
+ tContent.append(line).append("\n");
+ if (tContent.length()>=pos) {
+ in.close();
+ return tContent.toString();
+ }
+ }
+ in.close();
+ } catch (FileNotFoundException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ return tContent.toString();
+ }
+
+ public static boolean canWrite(File file){
+ boolean canWrite = file.canWrite();
+ if(!canWrite){
+ DocumentFile documentFile = DocumentUtil.getDocumentFile(file,false, activity);
+ if(documentFile!=null)
+ canWrite = documentFile.canWrite();
+ }
+ return canWrite;
+ }
+
+ public static FileOutputStream getFileOutputStream(File file,boolean append){
+ FileOutputStream fileOutputStream;
+ try {
+ fileOutputStream = new FileOutputStream(file, append);
+ } catch (IOException e){
+ fileOutputStream = (FileOutputStream) DocumentUtil.getOutputStream(activity,file,append);
+ }
+ return fileOutputStream;
+ }
+
+ public static FileOutputStream getFileOutputStream(File file){
+ FileOutputStream fileOutputStream;
+ try {
+ fileOutputStream = new FileOutputStream(file);
+ } catch (IOException e){
+ fileOutputStream = (FileOutputStream) DocumentUtil.getOutputStream(activity,file);
+ }
+ return fileOutputStream;
+ }
+
+ public static void writeToFile(String filePath, String text, boolean append) {
+ FileOutputStream fOut;
+ try{
+ try {
+ fOut = new FileOutputStream(filePath,append);
+ fOut.write(text.getBytes());
+ } catch (IOException e) {
+ DocumentFile file = DocumentUtil.getDocumentFile(new File(filePath), false, activity);
+ String mode;
+ if(append)
+ mode = "wa";
+ else mode = "wt";
+ fOut = (FileOutputStream) activity.getContentResolver().openOutputStream(file.getUri(),mode);
+ fOut.write(text.getBytes());
+ }
+ fOut.flush();
+ fOut.close();
+ } catch (IOException iox) {
+ iox.printStackTrace();
+ }
+ }
+
+ public static void writeToFile(String filePath, String text) {
+ FileOutputStream fOut;
+ try{
+ try {
+ fOut = new FileOutputStream(filePath);
+ fOut.write(text.getBytes());
+ } catch (IOException e) {
+ DocumentFile file = DocumentUtil.getDocumentFile(new File(filePath), false, activity);
+ fOut = (FileOutputStream) activity.getContentResolver().openOutputStream(file.getUri(),"wt");
+ fOut.write(text.getBytes());
+ }
+ fOut.flush();
+ fOut.close();
+ } catch (IOException iox) {
+ iox.printStackTrace();
+ }
+ }
+
+ public static File fileAutoMkParent(File file) throws Exception {
+ File parent = file.getParentFile();
+ if(parent == null)
+ throw new Exception("Cannot create directory : "+parent);
+ if (!parent.exists()) {
+ parent.mkdirs();
+ }
+ return file;
+ }
+
+ public static String fileAutoMkParent(String file) throws Exception {
+ return fileAutoMkParent(new File(file)).getAbsolutePath();
+ }
+}
diff --git a/qftplib/src/main/java/util/IoUtil.java b/qftplib/src/main/java/util/IoUtil.java
new file mode 100644
index 00000000..2c33628a
--- /dev/null
+++ b/qftplib/src/main/java/util/IoUtil.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * 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 util;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class IoUtil {
+ private static final int BUFFER_SIZE = 1024 * 8;
+
+ private IoUtil() {
+ // Utility class.
+ }
+
+ public static int copy(InputStream input, OutputStream output) throws Exception, IOException {
+ byte[] buffer = new byte[BUFFER_SIZE];
+
+ BufferedInputStream in = new BufferedInputStream(input, BUFFER_SIZE);
+ BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE);
+ int count = 0, n = 0;
+ try {
+ while ((n = in.read(buffer, 0, BUFFER_SIZE)) != -1) {
+ out.write(buffer, 0, n);
+ count += n;
+ }
+ out.flush();
+ } finally {
+ try {
+ out.close();
+ } catch (IOException e) {
+ //Log.e(e.getMessage(), e);
+ }
+ try {
+ in.close();
+ } catch (IOException e) {
+ //Log.e(e.getMessage(), e);
+ }
+ }
+ return count;
+ }
+
+}
diff --git a/qftplib/src/main/res/drawable-hdpi/ftp_notification.png b/qftplib/src/main/res/drawable-hdpi/ftp_notification.png
new file mode 100644
index 00000000..160a2b8f
Binary files /dev/null and b/qftplib/src/main/res/drawable-hdpi/ftp_notification.png differ
diff --git a/qftplib/src/main/res/drawable-hdpi/ic_go.png b/qftplib/src/main/res/drawable-hdpi/ic_go.png
new file mode 100644
index 00000000..e70f0413
Binary files /dev/null and b/qftplib/src/main/res/drawable-hdpi/ic_go.png differ
diff --git a/qftplib/src/main/res/drawable-hdpi/ic_launcher.png b/qftplib/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 00000000..96a442e5
Binary files /dev/null and b/qftplib/src/main/res/drawable-hdpi/ic_launcher.png differ
diff --git a/qftplib/src/main/res/drawable-hdpi/ic_pause.png b/qftplib/src/main/res/drawable-hdpi/ic_pause.png
new file mode 100644
index 00000000..9661cfbb
Binary files /dev/null and b/qftplib/src/main/res/drawable-hdpi/ic_pause.png differ
diff --git a/qftplib/src/main/res/drawable-xxhdpi/ic_back.png b/qftplib/src/main/res/drawable-xxhdpi/ic_back.png
new file mode 100644
index 00000000..26a2eff2
Binary files /dev/null and b/qftplib/src/main/res/drawable-xxhdpi/ic_back.png differ
diff --git a/qftplib/src/main/res/drawable-xxhdpi/ic_launcher.png b/qftplib/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..71c6d760
Binary files /dev/null and b/qftplib/src/main/res/drawable-xxhdpi/ic_launcher.png differ
diff --git a/qftplib/src/main/res/layout/activity_preference.xml b/qftplib/src/main/res/layout/activity_preference.xml
new file mode 100644
index 00000000..353016be
--- /dev/null
+++ b/qftplib/src/main/res/layout/activity_preference.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qftplib/src/main/res/layout/ftp_widget.xml b/qftplib/src/main/res/layout/ftp_widget.xml
new file mode 100644
index 00000000..fa3c309e
--- /dev/null
+++ b/qftplib/src/main/res/layout/ftp_widget.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qftplib/src/main/res/values-v11/styles.xml b/qftplib/src/main/res/values-v11/styles.xml
new file mode 100644
index 00000000..cab48cd6
--- /dev/null
+++ b/qftplib/src/main/res/values-v11/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/qftplib/src/main/res/values-v14/styles.xml b/qftplib/src/main/res/values-v14/styles.xml
new file mode 100644
index 00000000..b6dd86ae
--- /dev/null
+++ b/qftplib/src/main/res/values-v14/styles.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/qftplib/src/main/res/values-zh-rCN/strings.xml b/qftplib/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..7f9e5fb9
--- /dev/null
+++ b/qftplib/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,57 @@
+
+QFtplib
+FTP 服务器
+运行
+启动 FTP 服务器
+FTP 服务运行在 %s
+无法启动 FTP 服务器
+设置
+用户名
+ftp
+密码
+ftp
+
+ 显示密码
+
+
+ 假
+
+高级设置...
+端口号
+停留在文件夹中
+/
+接受来自 WiFi 连接
+true
+接受来自网络代理连接
+false
+保持设备处于唤醒状态(全CPU速度)
+true
+额外
+帮助
+关于
+FTP 服务器帮助
+帮助信息
+FTP 服务器关于
+有关消息
+
+
+不能解析网址
+未知
+OK
+用户名必须由1个或多个字母,不允许使用其他字符。
+该密码必须由1个或多个字母数字字符,不允许使用其他字符。
+该端口号必须在范围为1~65535
+WifiStateReceiver
+FTP服务器启动
+FTP服务器正在运行
+服务器接受 FTP 连接
+连接
+已连接
+失败
+无法访问
+断开的
+警告:存储不可用,你可能想卸载它
+FTP服务器控件
+扩展功能
+
+
diff --git a/qftplib/src/main/res/values/colors.xml b/qftplib/src/main/res/values/colors.xml
new file mode 100644
index 00000000..17016d2d
--- /dev/null
+++ b/qftplib/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #FF4A4A4A
+ #FF363636
+ #FF4BAC07
+ #FFFFFF
+
\ No newline at end of file
diff --git a/qftplib/src/main/res/values/strings.xml b/qftplib/src/main/res/values/strings.xml
new file mode 100644
index 00000000..6405565d
--- /dev/null
+++ b/qftplib/src/main/res/values/strings.xml
@@ -0,0 +1,72 @@
+
+ QFtplib
+ FTP Server
+ Running
+ Start FTP server
+ FTP server running at %s
+ Failed to start the FTP server
+ Settings
+ Username
+ ftp
+ Password
+ ftp
+
+ Display password
+
+
+ false
+
+ Advanced Settings...
+ Port Number
+ 2121
+ Stay in folder
+ /
+ Accept connection from wifi
+ true
+ Accept connection from net proxy
+ false
+ Keep device awake (full CPU speed)
+ true
+ Extra
+ Help...
+ About...
+ FTP Server Help
+ HELP Message
+ FTP Server About
+ ABOUT Message
+
+
+ Can\'t retreive url
+ Notice
+
+ unknown
+ OK
+ The username must consist of 1 or more letters. No other characters are allowed.
+ The password must consist of 1 or more alphanumeric characters. No other characters are allowed.
+ The port number must be in the range 1 to 65535.
+ WifiStateReceiver
+ FTP Server started
+ FTP Server is running
+ Server is accepting FTP connections.
+ connecting
+ connected
+ failed
+ unreachable
+ disconnected
+ Warning: storage is not available. You may want to unmount it.
+ FTP Server Widget
+ FTP setting
+ FTP Server Setting
+ Extensions
+
+
+ reset_storage
+ running_state
+ username
+ password
+ portNum
+ show_password
+ chrootDir
+ stayAwake
+ about
+
diff --git a/qftplib/src/main/res/values/styles.xml b/qftplib/src/main/res/values/styles.xml
new file mode 100644
index 00000000..c6d69f50
--- /dev/null
+++ b/qftplib/src/main/res/values/styles.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qftplib/src/main/res/xml/ftp_preferences.xml b/qftplib/src/main/res/xml/ftp_preferences.xml
new file mode 100644
index 00000000..aeb50739
--- /dev/null
+++ b/qftplib/src/main/res/xml/ftp_preferences.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qftplib/src/main/res/xml/header_preferences.xml b/qftplib/src/main/res/xml/header_preferences.xml
new file mode 100644
index 00000000..ea336944
--- /dev/null
+++ b/qftplib/src/main/res/xml/header_preferences.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/qpypluginman/build.gradle b/qpypluginman/build.gradle
index 3d455a43..935af90d 100644
--- a/qpypluginman/build.gradle
+++ b/qpypluginman/build.gradle
@@ -1,31 +1,33 @@
-apply plugin: 'com.android.library'
-
-android {
- compileSdkVersion rootProject.ext.compileSdkVersion
-
- defaultConfig {
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- lintOptions {
- abortOnError false
- }
-}
-
-dependencies {
-// api fileTree(include: ['*.jar'], dir: 'libs')
- //testApi 'junit:junit:4.12'
- api rootProject.ext.libOkHttp3
- api rootProject.ext.libRxJava
- api rootProject.ext.libRxAndroid
- api rootProject.ext.libFileDownloaderLib
- api rootProject.ext.libSupportV4
-}
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ lintOptions {
+ abortOnError false
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+}
+
+dependencies {
+// api fileTree(include: ['*.jar'], dir: 'libs')
+ //testApi 'junit:junit:4.12'
+ api rootProject.ext.libOkHttp3
+ api rootProject.ext.libRxJava
+ api rootProject.ext.libRxAndroid
+ api rootProject.ext.libFileDownloaderLib
+ api rootProject.ext.libSupportV4
+}
diff --git a/qpypluginman/proguard-rules.pro b/qpypluginman/proguard-rules.pro
index 36693fa1..ce612161 100644
--- a/qpypluginman/proguard-rules.pro
+++ b/qpypluginman/proguard-rules.pro
@@ -1,17 +1,17 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in C:\Users\Jay\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
-# You can edit the include mPath and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in C:\Users\Jay\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
+# You can edit the include mPath and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/qpypluginman/src/main/AndroidManifest.xml b/qpypluginman/src/main/AndroidManifest.xml
index 3592f067..d010ff2f 100644
--- a/qpypluginman/src/main/AndroidManifest.xml
+++ b/qpypluginman/src/main/AndroidManifest.xml
@@ -1,8 +1,8 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/Updater.java b/qpypluginman/src/main/java/com/quseit/common/updater/Updater.java
index 08de8c45..3f814ef5 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/Updater.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/Updater.java
@@ -1,178 +1,178 @@
-package com.quseit.common.updater;
-
-
-import android.app.Application;
-import android.content.Context;
-
-import com.quseit.common.updater.convertor.Convertor;
-import com.quseit.common.updater.downloader.DefaultDownloader;
-import com.quseit.common.updater.downloader.Downloader;
-import com.quseit.common.updater.service.DefaultService;
-import com.quseit.common.updater.service.Service;
-import com.quseit.common.updater.updatepkg.UpdatePackage;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-import rx.Observable;
-import rx.Subscriber;
-import rx.android.schedulers.AndroidSchedulers;
-import rx.functions.Action1;
-import rx.functions.Func1;
-import rx.schedulers.Schedulers;
-
-public class Updater {
- public static final String TAG = "Updater";
- private static List pkgs;
-
- private static Service service;
- private static Convertor convertor;
- private static Downloader downloader;
-
- private static String url;
- private static Context context;
-
- public static synchronized void init(Application app, String url, Convertor convertor) {
- Updater.context = app.getApplicationContext();
- Updater.url = url;
- Updater.convertor = convertor;
-
- Updater.service = new DefaultService();
- Updater.downloader = new DefaultDownloader(app.getApplicationContext());
- }
-
- // download service
- public static void downloadAs(String file, String url, String savePath) {
- Updater.downloader.download(file, url, savePath, new Downloader.Callback() {
- @Override
- public void pending(String name) {
-
- }
-
- @Override
- public void complete(String name, File installer) {
- }
-
- @Override
- public void error(String err) {
-
- }
- });
- }
- // update servivce
- public static void checkUpdate(final CheckUpdateCallback callback) {
- Observable
- .create(new Observable.OnSubscribe() {
- @Override
- public void call(Subscriber super String> subscriber) {
- try {
- String response = service.request(url);
- subscriber.onNext(response);
- } catch (IOException e) {
- subscriber.onError(e);
- } finally {
- subscriber.onCompleted();
- }
- }
- })
- .subscribeOn(Schedulers.io())
- .observeOn(Schedulers.computation())
- .map(new Func1>() {
- @Override
- public List extends UpdatePackage> call(String response) {
- return convertor.transform(response);
- }
- })
- .flatMap(new Func1, Observable>() {
- @Override
- public Observable call(List extends UpdatePackage> updatePkgs) {
- return Observable.from(updatePkgs);
- }
- })
- .filter(new Func1() {
- @Override
- public Boolean call(UpdatePackage updatePackage) {
- return updatePackage.checkVersion();
- }
- })
- .toList()
- .doOnNext(new Action1>() {
- @Override
- public void call(List pkgs) {
- // 保存最新的获取的更新包
- Updater.pkgs = pkgs;
- }
- })
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(
- new Action1>() {
- @Override
- public void call(List updatePackages) {
- if (!updatePackages.isEmpty()) {
- callback.hasUpdate(updatePackages);
- } else {
- callback.noneUpdate();
- }
- }
- },
- new Action1() {
- @Override
- public void call(Throwable throwable) {
- throwable.printStackTrace();
- callback.error(throwable);
- }
- });
- }
-
- public static void update(UpdatePackage pkg) {
- downloadAndInstall(pkg);
- }
-
- public static void update() {
- // 检查是否调用过 checkUpdate
- if (Updater.pkgs == null) {
- return;
- }
-
- downloadAndInstall(pkgs);
- }
-
- private static void downloadAndInstall(final UpdatePackage pkg) {
- downloader.download(pkg.getName(), pkg.getDownloadUrl(),
- new Downloader.Callback() {
- @Override
- public void pending(String name) {
-
- }
-
- @Override
- public void complete(String name, File installer) {
- pkg.install(installer);
- }
-
- @Override
- public void error(String err) {
-
- }
- });
- }
-
- private static void downloadAndInstall(List pkgs) {
- for (UpdatePackage pkg : pkgs) {
- downloadAndInstall(pkg);
- }
- }
-
- public static Context getContext() {
- return context;
- }
-
- public interface CheckUpdateCallback {
- void hasUpdate(List pkgs);
-
- void noneUpdate();
-
- void error(Throwable e);
- }
-}
+package com.quseit.common.updater;
+
+
+import android.app.Application;
+import android.content.Context;
+
+import com.quseit.common.updater.convertor.Convertor;
+import com.quseit.common.updater.downloader.DefaultDownloader;
+import com.quseit.common.updater.downloader.Downloader;
+import com.quseit.common.updater.service.DefaultService;
+import com.quseit.common.updater.service.Service;
+import com.quseit.common.updater.updatepkg.UpdatePackage;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import rx.Observable;
+import rx.Subscriber;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action1;
+import rx.functions.Func1;
+import rx.schedulers.Schedulers;
+
+public class Updater {
+ public static final String TAG = "Updater";
+ private static List pkgs;
+
+ private static Service service;
+ private static Convertor convertor;
+ private static Downloader downloader;
+
+ private static String url;
+ private static Context context;
+
+ public static synchronized void init(Application app, String url, Convertor convertor) {
+ Updater.context = app.getApplicationContext();
+ Updater.url = url;
+ Updater.convertor = convertor;
+
+ Updater.service = new DefaultService();
+ Updater.downloader = new DefaultDownloader(app.getApplicationContext());
+ }
+
+ // download service
+ public static void downloadAs(String file, String url, String savePath) {
+ Updater.downloader.download(file, url, savePath, new Downloader.Callback() {
+ @Override
+ public void pending(String name) {
+
+ }
+
+ @Override
+ public void complete(String name, File installer) {
+ }
+
+ @Override
+ public void error(String err) {
+
+ }
+ });
+ }
+ // update servivce
+ public static void checkUpdate(final CheckUpdateCallback callback) {
+ Observable
+ .create(new Observable.OnSubscribe() {
+ @Override
+ public void call(Subscriber super String> subscriber) {
+ try {
+ String response = service.request(url);
+ subscriber.onNext(response);
+ } catch (IOException e) {
+ subscriber.onError(e);
+ } finally {
+ subscriber.onCompleted();
+ }
+ }
+ })
+ .subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.computation())
+ .map(new Func1>() {
+ @Override
+ public List extends UpdatePackage> call(String response) {
+ return convertor.transform(response);
+ }
+ })
+ .flatMap(new Func1, Observable>() {
+ @Override
+ public Observable call(List extends UpdatePackage> updatePkgs) {
+ return Observable.from(updatePkgs);
+ }
+ })
+ .filter(new Func1() {
+ @Override
+ public Boolean call(UpdatePackage updatePackage) {
+ return updatePackage.checkVersion();
+ }
+ })
+ .toList()
+ .doOnNext(new Action1>() {
+ @Override
+ public void call(List pkgs) {
+ // 保存最新的获取的更新包
+ Updater.pkgs = pkgs;
+ }
+ })
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ new Action1>() {
+ @Override
+ public void call(List updatePackages) {
+ if (!updatePackages.isEmpty()) {
+ callback.hasUpdate(updatePackages);
+ } else {
+ callback.noneUpdate();
+ }
+ }
+ },
+ new Action1() {
+ @Override
+ public void call(Throwable throwable) {
+ throwable.printStackTrace();
+ callback.error(throwable);
+ }
+ });
+ }
+
+ public static void update(UpdatePackage pkg) {
+ downloadAndInstall(pkg);
+ }
+
+ public static void update() {
+ // 检查是否调用过 checkUpdate
+ if (Updater.pkgs == null) {
+ return;
+ }
+
+ downloadAndInstall(pkgs);
+ }
+
+ private static void downloadAndInstall(final UpdatePackage pkg) {
+ downloader.download(pkg.getName(), pkg.getDownloadUrl(),
+ new Downloader.Callback() {
+ @Override
+ public void pending(String name) {
+
+ }
+
+ @Override
+ public void complete(String name, File installer) {
+ pkg.install(installer);
+ }
+
+ @Override
+ public void error(String err) {
+
+ }
+ });
+ }
+
+ private static void downloadAndInstall(List pkgs) {
+ for (UpdatePackage pkg : pkgs) {
+ downloadAndInstall(pkg);
+ }
+ }
+
+ public static Context getContext() {
+ return context;
+ }
+
+ public interface CheckUpdateCallback {
+ void hasUpdate(List pkgs);
+
+ void noneUpdate();
+
+ void error(Throwable e);
+ }
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/callback/DialogCallback.java b/qpypluginman/src/main/java/com/quseit/common/updater/callback/DialogCallback.java
index 53f7b155..a13454cb 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/callback/DialogCallback.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/callback/DialogCallback.java
@@ -1,138 +1,138 @@
-package com.quseit.common.updater.callback;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v4.app.FragmentActivity;
-import android.widget.Toast;
-
-import com.quseit.common.updater.R;
-import com.quseit.common.updater.Updater;
-import com.quseit.common.updater.updatepkg.UpdatePackage;
-
-import java.util.List;
-
-public class DialogCallback implements Updater.CheckUpdateCallback {
- private FragmentActivity mActivity;
- private boolean mIsSilence;
-
- public DialogCallback(FragmentActivity activity, boolean isSilence) {
- mActivity = activity;
- mIsSilence = isSilence;
- }
-
- @Override
- public void hasUpdate(List pkgs) {
- StringBuilder description = new StringBuilder();
- for (UpdatePackage pkg : pkgs) {
- description
- .append(pkg.getName())
- .append(":")
- .append("\n")
- .append(pkg.getVersionDescription())
- .append("\n\n");
- }
- description.delete(description.length() - 2, description.length());
-
- DialogFragment dialog = SimpleReminderDialogFragment.newInstance(description.toString());
- try {
- dialog.show(mActivity.getSupportFragmentManager(), SimpleReminderDialogFragment.TAG);
- } catch (Exception e) {
- // mActivity 不可用时忽略
- }
- }
-
- @Override
- public void noneUpdate() {
- try {
- if (!mIsSilence) {
- Toast.makeText(mActivity, R.string.latest_version, Toast.LENGTH_SHORT).show();
- }
- } catch (Exception e) {
- // mActivity 不可用时忽略
- }
- }
-
- @Override
- public void error(Throwable e) {
- try {
- if (!mIsSilence) {
- Toast.makeText(mActivity, R.string.check_update_error, Toast.LENGTH_SHORT).show();
- }
- } catch (Exception e1) {
- // mActivity 不可用时忽略
- }
- }
-
- public static class SimpleReminderDialogFragment extends DialogFragment {
- public static final String TAG = "SimpleReminderDialogFragment";
- private static final String DESCRIPTION = "description";
-
- public static SimpleReminderDialogFragment newInstance(String description) {
- Bundle args = new Bundle();
- args.putString(DESCRIPTION, description);
- SimpleReminderDialogFragment fragment = new SimpleReminderDialogFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new AlertDialog.Builder(getActivity())
- .setTitle(R.string.has_update)
- .setPositiveButton(R.string.update, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- Updater.update();
- dismiss();
- }
- })
- .setNegativeButton(R.string.show_detail, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- String description = getArguments().getString(DESCRIPTION);
- DialogFragment dialog = DetailReminderDialogFragment.newInstance(description);
- dialog.show(getFragmentManager(), DetailReminderDialogFragment.TAG);
- dismiss();
- }
- })
- .create();
- }
- }
-
- public static class DetailReminderDialogFragment extends DialogFragment {
- public static final String TAG = "DetailReminderDialogFragment";
- private static final String DESCRIPTION = "description";
-
- public static DetailReminderDialogFragment newInstance(String description) {
- Bundle args = new Bundle();
- args.putString(DESCRIPTION, description);
- DetailReminderDialogFragment fragment = new DetailReminderDialogFragment();
- fragment.setArguments(args);
- return fragment;
- }
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new AlertDialog.Builder(getActivity())
- .setTitle(R.string.has_update)
- .setMessage(getArguments().getString(DESCRIPTION))
- .setPositiveButton(R.string.update, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- Updater.update();
- dismiss();
- }
- })
- .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialogInterface, int i) {
- dismiss();
- }
- })
- .create();
- }
- }
-}
+package com.quseit.common.updater.callback;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import android.widget.Toast;
+
+import com.quseit.common.updater.R;
+import com.quseit.common.updater.Updater;
+import com.quseit.common.updater.updatepkg.UpdatePackage;
+
+import java.util.List;
+
+public class DialogCallback implements Updater.CheckUpdateCallback {
+ private FragmentActivity mActivity;
+ private boolean mIsSilence;
+
+ public DialogCallback(FragmentActivity activity, boolean isSilence) {
+ mActivity = activity;
+ mIsSilence = isSilence;
+ }
+
+ @Override
+ public void hasUpdate(List pkgs) {
+ StringBuilder description = new StringBuilder();
+ for (UpdatePackage pkg : pkgs) {
+ description
+ .append(pkg.getName())
+ .append(":")
+ .append("\n")
+ .append(pkg.getVersionDescription())
+ .append("\n\n");
+ }
+ description.delete(description.length() - 2, description.length());
+
+ DialogFragment dialog = SimpleReminderDialogFragment.newInstance(description.toString());
+ try {
+ dialog.show(mActivity.getSupportFragmentManager(), SimpleReminderDialogFragment.TAG);
+ } catch (Exception e) {
+ // mActivity 不可用时忽略
+ }
+ }
+
+ @Override
+ public void noneUpdate() {
+ try {
+ if (!mIsSilence) {
+ Toast.makeText(mActivity, R.string.latest_version, Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e) {
+ // mActivity 不可用时忽略
+ }
+ }
+
+ @Override
+ public void error(Throwable e) {
+ try {
+ if (!mIsSilence) {
+ Toast.makeText(mActivity, R.string.check_update_error, Toast.LENGTH_SHORT).show();
+ }
+ } catch (Exception e1) {
+ // mActivity 不可用时忽略
+ }
+ }
+
+ public static class SimpleReminderDialogFragment extends DialogFragment {
+ public static final String TAG = "SimpleReminderDialogFragment";
+ private static final String DESCRIPTION = "description";
+
+ public static SimpleReminderDialogFragment newInstance(String description) {
+ Bundle args = new Bundle();
+ args.putString(DESCRIPTION, description);
+ SimpleReminderDialogFragment fragment = new SimpleReminderDialogFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.has_update)
+ .setPositiveButton(R.string.update, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Updater.update();
+ dismiss();
+ }
+ })
+ .setNegativeButton(R.string.show_detail, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ String description = getArguments().getString(DESCRIPTION);
+ DialogFragment dialog = DetailReminderDialogFragment.newInstance(description);
+ dialog.show(getFragmentManager(), DetailReminderDialogFragment.TAG);
+ dismiss();
+ }
+ })
+ .create();
+ }
+ }
+
+ public static class DetailReminderDialogFragment extends DialogFragment {
+ public static final String TAG = "DetailReminderDialogFragment";
+ private static final String DESCRIPTION = "description";
+
+ public static DetailReminderDialogFragment newInstance(String description) {
+ Bundle args = new Bundle();
+ args.putString(DESCRIPTION, description);
+ DetailReminderDialogFragment fragment = new DetailReminderDialogFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.has_update)
+ .setMessage(getArguments().getString(DESCRIPTION))
+ .setPositiveButton(R.string.update, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ Updater.update();
+ dismiss();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dismiss();
+ }
+ })
+ .create();
+ }
+ }
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/convertor/Convertor.java b/qpypluginman/src/main/java/com/quseit/common/updater/convertor/Convertor.java
index 972d93da..9bdda98c 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/convertor/Convertor.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/convertor/Convertor.java
@@ -1,10 +1,10 @@
-package com.quseit.common.updater.convertor;
-
-
-import com.quseit.common.updater.updatepkg.UpdatePackage;
-
-import java.util.List;
-
-public interface Convertor {
- List extends UpdatePackage> transform(String response);
-}
+package com.quseit.common.updater.convertor;
+
+
+import com.quseit.common.updater.updatepkg.UpdatePackage;
+
+import java.util.List;
+
+public interface Convertor {
+ List extends UpdatePackage> transform(String response);
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/downloader/DefaultDownloader.java b/qpypluginman/src/main/java/com/quseit/common/updater/downloader/DefaultDownloader.java
index 3cd3182e..a0e5bd89 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/downloader/DefaultDownloader.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/downloader/DefaultDownloader.java
@@ -1,104 +1,104 @@
-package com.quseit.common.updater.downloader;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
-import android.os.Environment;
-import android.support.v4.app.NotificationCompat;
-import android.util.Log;
-
-import com.liulishuo.filedownloader.BaseDownloadTask;
-import com.liulishuo.filedownloader.FileDownloadListener;
-import com.liulishuo.filedownloader.FileDownloader;
-import com.quseit.common.updater.R;
-
-import java.io.File;
-
-import static android.os.Environment.getExternalStoragePublicDirectory;
-
-public class DefaultDownloader implements Downloader {
- public static final String TAG = "DefaultDownloader";
- public final String DEFAULT_PATH;
- private final Context context;
- private final NotificationManager notificationManager;
-
- public DefaultDownloader(Context context) {
- this.context = context;
- FileDownloader.init(context);
- notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- DEFAULT_PATH = getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
- }
-
- @Override
- public void download(final String name, String url, final Callback callback) {
- String path = DEFAULT_PATH + "/" + name;
- download(name, url, path, callback);
- }
-
- @Override
- public void download(final String name, String url, final String path, final Callback callback) {
- Log.d(TAG, "download:"+name+":"+url+"["+path+"]");
- FileDownloader.getImpl()
- .create(url)
- .setPath(path, false)
- .setCallbackProgressTimes(2000)
- .setListener(new FileDownloadListener() {
- @Override
- protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {
- Log.d(TAG, "download:pending:");
- callback.pending(name);
- }
-
- @Override
- protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {
- Log.d(TAG, "download:progress:");
-
- Notification notification = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_cloud_download_black_24dp)
- .setContentTitle(context.getText(R.string.downloading))
- .setContentText(name)
- .setProgress(totalBytes, soFarBytes, false)
- .build();
- notificationManager.notify(name.hashCode(), notification);
- }
-
- @Override
- protected void completed(BaseDownloadTask task) {
- Log.d(TAG, "download:completed:");
-
- //notificationManager.cancel(name.hashCode());
- Notification notification = new NotificationCompat.Builder(context)
- .setSmallIcon(R.drawable.ic_cloud_download_black_24dp)
- .setContentTitle(context.getText(R.string.downloaded))
- .setContentText(name)
- .setProgress(100, 100, false)
- .build();
-
- notificationManager.notify(name.hashCode(),notification);
- File file = new File(task.getTargetFilePath());
- callback.complete(name, file);
- }
-
- @Override
- protected void paused(BaseDownloadTask task, int soFarBytes, int totalBytes) {
- Log.d(TAG, "download:paused:");
-
- }
-
- @Override
- protected void error(BaseDownloadTask task, Throwable e) {
- Log.d(TAG, "download:error:"+e.getLocalizedMessage());
- callback.error(e.getLocalizedMessage());
-
- }
-
- @Override
- protected void warn(BaseDownloadTask task) {
- Log.d(TAG, "download:warn:");
-
- }
- })
- .start();
-
- }
+package com.quseit.common.updater.downloader;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Environment;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+
+import com.liulishuo.filedownloader.BaseDownloadTask;
+import com.liulishuo.filedownloader.FileDownloadListener;
+import com.liulishuo.filedownloader.FileDownloader;
+import com.quseit.common.updater.R;
+
+import java.io.File;
+
+import static android.os.Environment.getExternalStoragePublicDirectory;
+
+public class DefaultDownloader implements Downloader {
+ public static final String TAG = "DefaultDownloader";
+ public final String DEFAULT_PATH;
+ private final Context context;
+ private final NotificationManager notificationManager;
+
+ public DefaultDownloader(Context context) {
+ this.context = context;
+ FileDownloader.init(context);
+ notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ DEFAULT_PATH = getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
+ }
+
+ @Override
+ public void download(final String name, String url, final Callback callback) {
+ String path = DEFAULT_PATH + "/" + name;
+ download(name, url, path, callback);
+ }
+
+ @Override
+ public void download(final String name, String url, final String path, final Callback callback) {
+ Log.d(TAG, "download:"+name+":"+url+"["+path+"]");
+ FileDownloader.getImpl()
+ .create(url)
+ .setPath(path, false)
+ .setCallbackProgressTimes(2000)
+ .setListener(new FileDownloadListener() {
+ @Override
+ protected void pending(BaseDownloadTask task, int soFarBytes, int totalBytes) {
+ Log.d(TAG, "download:pending:");
+ callback.pending(name);
+ }
+
+ @Override
+ protected void progress(BaseDownloadTask task, int soFarBytes, int totalBytes) {
+ Log.d(TAG, "download:progress:");
+
+ Notification notification = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.ic_cloud_download_black_24dp)
+ .setContentTitle(context.getText(R.string.downloading))
+ .setContentText(name)
+ .setProgress(totalBytes, soFarBytes, false)
+ .build();
+ notificationManager.notify(name.hashCode(), notification);
+ }
+
+ @Override
+ protected void completed(BaseDownloadTask task) {
+ Log.d(TAG, "download:completed:");
+
+ //notificationManager.cancel(name.hashCode());
+ Notification notification = new NotificationCompat.Builder(context)
+ .setSmallIcon(R.drawable.ic_cloud_download_black_24dp)
+ .setContentTitle(context.getText(R.string.downloaded))
+ .setContentText(name)
+ .setProgress(100, 100, false)
+ .build();
+
+ notificationManager.notify(name.hashCode(),notification);
+ File file = new File(task.getTargetFilePath());
+ callback.complete(name, file);
+ }
+
+ @Override
+ protected void paused(BaseDownloadTask task, int soFarBytes, int totalBytes) {
+ Log.d(TAG, "download:paused:");
+
+ }
+
+ @Override
+ protected void error(BaseDownloadTask task, Throwable e) {
+ Log.d(TAG, "download:error:"+e.getLocalizedMessage());
+ callback.error(e.getLocalizedMessage());
+
+ }
+
+ @Override
+ protected void warn(BaseDownloadTask task) {
+ Log.d(TAG, "download:warn:");
+
+ }
+ })
+ .start();
+
+ }
}
\ No newline at end of file
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/downloader/Downloader.java b/qpypluginman/src/main/java/com/quseit/common/updater/downloader/Downloader.java
index 260db300..70e88cc7 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/downloader/Downloader.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/downloader/Downloader.java
@@ -1,15 +1,15 @@
-package com.quseit.common.updater.downloader;
-
-import java.io.File;
-
-public interface Downloader {
- void download(String name, String url, Callback callback);
-
- void download(String name, String url, String savePath, Callback callback);
-
- interface Callback {
- void pending(String name);
- void complete(String name, File installer);
- void error(String err);
- }
-}
+package com.quseit.common.updater.downloader;
+
+import java.io.File;
+
+public interface Downloader {
+ void download(String name, String url, Callback callback);
+
+ void download(String name, String url, String savePath, Callback callback);
+
+ interface Callback {
+ void pending(String name);
+ void complete(String name, File installer);
+ void error(String err);
+ }
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/service/DefaultService.java b/qpypluginman/src/main/java/com/quseit/common/updater/service/DefaultService.java
index 3fde70af..48cab3b4 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/service/DefaultService.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/service/DefaultService.java
@@ -1,22 +1,22 @@
-package com.quseit.common.updater.service;
-
-import java.io.IOException;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-
-public class DefaultService implements Service {
- private final OkHttpClient client;
-
- public DefaultService() {
- client = new OkHttpClient();
- }
-
- @Override
- public String request(String url) throws IOException {
- Request request = new Request.Builder()
- .url(url)
- .build();
- return client.newCall(request).execute().body().string();
- }
-}
+package com.quseit.common.updater.service;
+
+import java.io.IOException;
+
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+
+public class DefaultService implements Service {
+ private final OkHttpClient client;
+
+ public DefaultService() {
+ client = new OkHttpClient();
+ }
+
+ @Override
+ public String request(String url) throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
+ return client.newCall(request).execute().body().string();
+ }
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/service/Service.java b/qpypluginman/src/main/java/com/quseit/common/updater/service/Service.java
index 6690791c..39e6ba85 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/service/Service.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/service/Service.java
@@ -1,7 +1,7 @@
-package com.quseit.common.updater.service;
-
-import java.io.IOException;
-
-public interface Service {
- String request(String url) throws IOException;
-}
+package com.quseit.common.updater.service;
+
+import java.io.IOException;
+
+public interface Service {
+ String request(String url) throws IOException;
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/Apk.java b/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/Apk.java
index 55c3a465..7a9d47cc 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/Apk.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/Apk.java
@@ -1,70 +1,70 @@
-package com.quseit.common.updater.updatepkg;
-
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.net.Uri;
-
-import com.quseit.common.updater.Updater;
-
-import java.io.File;
-
-public class Apk implements UpdatePackage {
- private String name;
- private String version;
- private int versionCode;
- private String description;
- private String url;
-
- public Apk(String name, String version, int versionCode, String description, String url) {
- this.name = name;
- this.version = version;
- this.versionCode = versionCode;
- this.description = description;
- this.url = url;
- }
-
- @Override
- public String getName() {
- return this.name;
- }
-
- @Override
- public String getVersion() {
- return String.valueOf(this.version);
- }
-
- public String getVersionDescription() {
- return this.description;
- }
-
- @Override
- public String getDownloadUrl() {
- return this.url;
- }
-
- @Override
- public boolean checkVersion() {
- Context context = Updater.getContext();
- PackageManager packageManager = context.getPackageManager();
- PackageInfo packageInfo = null;
- try {
- packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
-
- } catch (PackageManager.NameNotFoundException e) {
- e.printStackTrace();
- }
- int localVersion = packageInfo.versionCode;
- return this.versionCode > localVersion;
- }
-
- @Override
- public void install(File installFile) {
- Uri uri = Uri.fromFile(installFile);
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(uri, "application/vnd.android.package-archive");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- Updater.getContext().startActivity(intent);
- }
-}
+package com.quseit.common.updater.updatepkg;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+
+import com.quseit.common.updater.Updater;
+
+import java.io.File;
+
+public class Apk implements UpdatePackage {
+ private String name;
+ private String version;
+ private int versionCode;
+ private String description;
+ private String url;
+
+ public Apk(String name, String version, int versionCode, String description, String url) {
+ this.name = name;
+ this.version = version;
+ this.versionCode = versionCode;
+ this.description = description;
+ this.url = url;
+ }
+
+ @Override
+ public String getName() {
+ return this.name;
+ }
+
+ @Override
+ public String getVersion() {
+ return String.valueOf(this.version);
+ }
+
+ public String getVersionDescription() {
+ return this.description;
+ }
+
+ @Override
+ public String getDownloadUrl() {
+ return this.url;
+ }
+
+ @Override
+ public boolean checkVersion() {
+ Context context = Updater.getContext();
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ int localVersion = packageInfo.versionCode;
+ return this.versionCode > localVersion;
+ }
+
+ @Override
+ public void install(File installFile) {
+ Uri uri = Uri.fromFile(installFile);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, "application/vnd.android.package-archive");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Updater.getContext().startActivity(intent);
+ }
+}
diff --git a/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/UpdatePackage.java b/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/UpdatePackage.java
index e274e6fe..cf5d1d49 100644
--- a/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/UpdatePackage.java
+++ b/qpypluginman/src/main/java/com/quseit/common/updater/updatepkg/UpdatePackage.java
@@ -1,18 +1,18 @@
-package com.quseit.common.updater.updatepkg;
-
-import java.io.File;
-
-public interface UpdatePackage {
-
- String getName();
-
- String getVersion();
-
- String getVersionDescription();
-
- String getDownloadUrl();
-
- boolean checkVersion();
-
- void install(File file);
-}
+package com.quseit.common.updater.updatepkg;
+
+import java.io.File;
+
+public interface UpdatePackage {
+
+ String getName();
+
+ String getVersion();
+
+ String getVersionDescription();
+
+ String getDownloadUrl();
+
+ boolean checkVersion();
+
+ void install(File file);
+}
diff --git a/qpypluginman/src/main/res/values-zh-rCn/strings.xml b/qpypluginman/src/main/res/values-zh-rCn/strings.xml
index a2154f42..53c75b7d 100644
--- a/qpypluginman/src/main/res/values-zh-rCn/strings.xml
+++ b/qpypluginman/src/main/res/values-zh-rCn/strings.xml
@@ -1,11 +1,11 @@
-
- 检查更新失败
- 您的应用已经是最新版本
- 应用有更新
- 更新
- 显示详情
- 取消
- 正在下载
- 下载完成
-
-
+
+ 检查更新失败
+ 您的应用已经是最新版本
+ 应用有更新
+ 更新
+ 显示详情
+ 取消
+ 正在下载
+ 下载完成
+
+
diff --git a/qpypluginman/src/main/res/values-zh-rTW/strings.xml b/qpypluginman/src/main/res/values-zh-rTW/strings.xml
deleted file mode 100644
index bc9f6cf0..00000000
--- a/qpypluginman/src/main/res/values-zh-rTW/strings.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
- 檢查更新失敗
- 已經更新
- 提示
- 有可使用的新版本
- 更新
- 顯示詳細內容
- 取消
- 正在下載
- 已下載
-
diff --git a/qpypluginman/src/main/res/values/strings.xml b/qpypluginman/src/main/res/values/strings.xml
index f700bcae..8c8f80aa 100644
--- a/qpypluginman/src/main/res/values/strings.xml
+++ b/qpypluginman/src/main/res/values/strings.xml
@@ -1,11 +1,11 @@
-
- Check update fail
- Already update
- New version available
- Update
- Show detail
- Cancel
- Downloading
- Completed
-
-
+
+ Check update fail
+ Already update
+ New version available
+ Update
+ Show detail
+ Cancel
+ Downloading
+ Completed
+
+
diff --git a/qpysdk/README b/qpysdk/README
index de389147..65507b00 100644
--- a/qpysdk/README
+++ b/qpysdk/README
@@ -1 +1 @@
-QPYTHON SDK
+QPYTHON SDK
diff --git a/qpysdk/build.gradle b/qpysdk/build.gradle
index deab3e87..69e1857e 100644
--- a/qpysdk/build.gradle
+++ b/qpysdk/build.gradle
@@ -1,45 +1,42 @@
-apply plugin: 'com.android.library'
-
-android {
- compileSdkVersion rootProject.ext.compileSdkVersion
- buildToolsVersion rootProject.ext.buildToolsVersion
-
- defaultConfig {
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
-
- ndk {
- abiFilters 'armeabi-v7a',"arm64-v8a"
- }
- }
- buildTypes {
- release {
- minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- }
- }
- externalNativeBuild {
- ndkBuild {
- path 'src/main/jni/Android.mk'
- }
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-
- lintOptions {
- abortOnError false
- }
-}
-
-
-dependencies {
-// api fileTree(dir: 'libs', include: ['*.jar'])
-// api files('libs/locale_platform.jar')
- api rootProject.ext.libGoogleGuava
- api project(':qbaselib')
- api project(':qpysl4a')
-}
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+
+ ndk {
+ abiFilters 'armeabi-v7a',"arm64-v8a"
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild {
+ ndkBuild {
+ path 'src/main/jni/Android.mk'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+}
+
+
+dependencies {
+// api fileTree(dir: 'libs', include: ['*.jar'])
+// api files('libs/locale_platform.jar')
+ api rootProject.ext.libGoogleGuava
+ api project(':qbaselib')
+ api project(':qpysl4a')
+}
diff --git a/qpysdk/proguard-rules.pro b/qpysdk/proguard-rules.pro
index 336f38c4..da4fd6d7 100644
--- a/qpysdk/proguard-rules.pro
+++ b/qpysdk/proguard-rules.pro
@@ -1,17 +1,17 @@
-# Add project specific ProGuard rules here.
-# By default, the flags in this file are appended to flags specified
-# in /Users/yhc/Library/Android/sdk/tools/proguard/proguard-android.txt
-# You can edit the include mPath and order by changing the proguardFiles
-# directive in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# Add any project specific keep options here:
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/yhc/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include mPath and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/qpysdk/src/main/AndroidManifest.xml b/qpysdk/src/main/AndroidManifest.xml
index 7ec0ded8..da3dbe25 100644
--- a/qpysdk/src/main/AndroidManifest.xml
+++ b/qpysdk/src/main/AndroidManifest.xml
@@ -1,11 +1,11 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/qpysdk/src/main/assets/android.js b/qpysdk/src/main/assets/android.js
deleted file mode 100644
index 9712150f..00000000
--- a/qpysdk/src/main/assets/android.js
+++ /dev/null
@@ -1,24 +0,0 @@
-var AndroidHelper = function() {
- this._callbacks = [],
- this._id = 0,
- this._call = function(method, params) {
- this._id += 1;
- var request = JSON.stringify({'id': this._id, 'method': method, 'params': params});
- var response = _rpc_wrapper.call(request);
- return eval("(" + response + ")");
- },
-
- this.registerCallback = function(event, receiver) {
- var id = this._callbacks.push(receiver) - 1;
- _callback_wrapper.register(event, id);
- },
-
- this._callback = function(id, data) {
- var receiver = this._callbacks[id];
- receiver(data);
- },
-
- this.dismiss = function() {
- _rpc_wrapper.dismiss();
- }
-};
diff --git a/qpysdk/src/main/assets/html/index.html b/qpysdk/src/main/assets/html/index.html
index aee35474..535c5858 100644
--- a/qpysdk/src/main/assets/html/index.html
+++ b/qpysdk/src/main/assets/html/index.html
@@ -1,56 +1,56 @@
-
-
-
-
-
-
-
-
-MIAndLib WebView
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+MIAndLib WebView
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qpysdk/src/main/assets/html/index.js b/qpysdk/src/main/assets/html/index.js
index 3b27247a..7624cdb3 100644
--- a/qpysdk/src/main/assets/html/index.js
+++ b/qpysdk/src/main/assets/html/index.js
@@ -1,29 +1,29 @@
-var total = 0
-function gotoApp() {
- total = total+1
- if (total>=10) {
- //setTimeout('milib.closeWait()',100);
- $('#mainEle').html('Fail to start QPyWebapp, please check manually.
');
- milib.onNext("timeout");
-
- } else {
- if (milib.isSrvOk("")) {
- setTimeout('window.location=milib.getSrv()',100);
- } else {
- if (total<15) {
- setTimeout('gotoApp()',3000);
- }
- }
- }
-}
-
-{
- $('#mainEle').html('');
- //milib.showWait();
- milib.loadConsole("");
-
- gotoApp();
-
- /*var droid = new AndroidHelper();
- droid.makeToast('HelloWorld')*/
-}
+var total = 0
+function gotoApp() {
+ total = total+1
+ if (total>=10) {
+ //setTimeout('milib.closeWait()',100);
+ $('#mainEle').html('Fail to start QPython WebApp, please click LOG button in the upper right to check . 启动QPython网页应用失败,请点击右上角LOG查看原因。
');
+ milib.onNext("timeout");
+
+ } else {
+ if (milib.isSrvOk("")) {
+ setTimeout('window.location=milib.getSrv()',100);
+ } else {
+ if (total<15) {
+ setTimeout('gotoApp()',3000);
+ }
+ }
+ }
+}
+
+{
+ $('#mainEle').html('');
+ //milib.showWait();
+ milib.loadConsole("");
+
+ gotoApp();
+
+ /*var droid = new AndroidHelper();
+ droid.makeToast('HelloWorld')*/
+}
diff --git a/qpysdk/src/main/assets/json2.js b/qpysdk/src/main/assets/json2.js
deleted file mode 100644
index d6b72ad5..00000000
--- a/qpysdk/src/main/assets/json2.js
+++ /dev/null
@@ -1,28 +0,0 @@
-if(!this.JSON){this.JSON={};}
-(function(){function f(n){return n<10?'0'+n:n;}
-if(typeof Date.prototype.toJSON!=='function'){Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+'-'+
-f(this.getUTCMonth()+1)+'-'+
-f(this.getUTCDate())+'T'+
-f(this.getUTCHours())+':'+
-f(this.getUTCMinutes())+':'+
-f(this.getUTCSeconds())+'Z':null;};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf();};}
-var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4);})+'"':'"'+string+'"';}
-function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==='object'&&typeof value.toJSON==='function'){value=value.toJSON(key);}
-if(typeof rep==='function'){value=rep.call(holder,key,value);}
-switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';}
-gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;icode{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::after,.was-validated .custom-file-input:valid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::after,.was-validated .custom-file-input:invalid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-item-next,.carousel-item-prev,.carousel-item.active{transition:none}}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
+/*!
+ * Bootstrap v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors
+ * Copyright 2011-2018 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(2.25rem + 2px);padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::after,.was-validated .custom-file-input:valid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::after,.was-validated .custom-file-input:invalid~.custom-file-label::after{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{height:calc(2.875rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{height:calc(1.8125rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:disabled~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media screen and (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:not(:disabled):not(.disabled){cursor:pointer}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{color:#000;text-decoration:none;opacity:.75}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-dialog-centered::before{display:block;height:calc(100vh - (.5rem * 2));content:""}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-dialog-centered::before{height:calc(100vh - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease}@media screen and (prefers-reduced-motion:reduce){.carousel-item-next,.carousel-item-prev,.carousel-item.active{transition:none}}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}}
/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/qpysdk/src/main/assets/stylesheets/css/sticky-footer.css b/qpysdk/src/main/assets/stylesheets/css/sticky-footer.css
index ebe29104..c62c8aad 100644
--- a/qpysdk/src/main/assets/stylesheets/css/sticky-footer.css
+++ b/qpysdk/src/main/assets/stylesheets/css/sticky-footer.css
@@ -1,28 +1,28 @@
-/* Sticky footer styles
--------------------------------------------------- */
-html {
- position: relative;
- min-height: 100%;
-}
-body {
- margin-bottom: 60px; /* Margin bottom by footer height */
-}
-.footer {
- position: absolute;
- bottom: 0;
- width: 100%;
- height: 60px; /* Set the fixed height of the footer here */
- line-height: 60px; /* Vertically center the text there */
- background-color: #f5f5f5;
-}
-
-
-/* Custom page CSS
--------------------------------------------------- */
-/* Not required for template or sticky footer method. */
-
-.container {
- width: auto;
- max-width: 680px;
- padding: 0 15px;
-}
+/* Sticky footer styles
+-------------------------------------------------- */
+html {
+ position: relative;
+ min-height: 100%;
+}
+body {
+ margin-bottom: 60px; /* Margin bottom by footer height */
+}
+.footer {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ height: 60px; /* Set the fixed height of the footer here */
+ line-height: 60px; /* Vertically center the text there */
+ background-color: #f5f5f5;
+}
+
+
+/* Custom page CSS
+-------------------------------------------------- */
+/* Not required for template or sticky footer method. */
+
+.container {
+ width: auto;
+ max-width: 680px;
+ padding: 0 15px;
+}
diff --git a/qpysdk/src/main/assets/stylesheets/css/style.css b/qpysdk/src/main/assets/stylesheets/css/style.css
index e3d543bb..68d50a52 100644
--- a/qpysdk/src/main/assets/stylesheets/css/style.css
+++ b/qpysdk/src/main/assets/stylesheets/css/style.css
@@ -1,141 +1,141 @@
-body,ul,li {
- padding:0;
- margin:0;
- border:0;
-}
-body {
- font-size:12px;
- -webkit-user-select:none;
- -webkit-text-size-adjust:none;
- font-family:helvetica;
-}
-#header { }
-#header a {
- color:#f3f3f3;
- text-decoration:none;
- font-weight:bold;
- text-shadow:0 -1px 0 rgba(0,0,0,0.5);
-}
-#footer { }
-#wrapper {
- /*position:absolute; z-index:1;*/
- top:45px; bottom:48px;
- text-align:center;
- width:100%;
- min-height:100px;
- overflow:auto;
-}
-/* ============================================================================= */
-
-#scroller {
- position:absolute; z-index:1;
-/* -webkit-touch-callout:none;*/
- -webkit-tap-highlight-color:rgba(0,0,0,0);
- width:100%;
- padding:0;
-}
-
-#scroller ul {
- list-style:none;
- padding:0;
- margin:0;
- width:100%;
- text-align:left;
-}
-
-#scroller li {
- padding:0 10px;
- height:40px;
- line-height:40px;
- border-bottom:1px solid #ccc;
- border-top:1px solid #fff;
- background-color:#fafafa;
- font-size:14px;
-}
-
-#myFrame {
- position:absolute;
- top:0; left:0;
-}
-.w30p { width:30% }
-/* ============================================================================= */
-
-#outer {
- width:100%;
- margin:auto;
- text-align:center;
-}
-.inner {
- display: table-cell;
- vertical-align:middle;
- text-align:center;
-
- width:90px;
- height:136px;
- margin:5px;
- border:1px solid #1f1f1f;
-}
-.inner_img {
- vertical-align:middle;
- max-width:90px;
- max-height:90px;
-}
-.inner_title {
- position:relative;
- float:left;
- width:90px;
- overflow:hidden;
- background:#1f1f1f;
- color:#fff;
- height:40px;
- padding:3px 0px;
-}
-* html .inner {display:inline} /* for ie*/
-html>body #outer {display:table} /*for mozilla */
-html>body .inner {display:table;float:left} /*for mozilla */
-@media all and (min-width: 0px) { /* opera 7 styles */
- html>body .inner {display:inline-block;float:none;}
-}
-/* ============================================================================= */
-
-.table { width:100% }
-.nowrap { overflow:hidden;white-space:nowrap }
-.tbl_title { font-size:16px;color:white;text-align:center;background:#1f1f1f; }
-.tpl_title_pre { height:38px;width:20px; background:#D70377;float:left;margin:-10px; }
-.title,a.title { font-size:16px;clolor:black }
-.desc { font-size:14px;color:grey }
-.pl15 { padding-left:15px }
-
-a {
-}
-
-a:link {
-color: black;
-text-decoration: none;
-}
-a:visited {
-color: black;
-text-decoration: none;
-}
-a:hover {
-color: black;
-text-decoration: none;
-}
-a:active {
-color: black;
-text-decoration: none;
-}
-
-/* ============================================================================= */
-
-.fl_left { float:left }
-.fl_right { float:right }
-.pl10 {padding-left:10px}
-.pl20 {padding-left:20px}
-.pt20 {padding-top:10px}
-.clear { clear:both }
-.act { font-size:20px;text-align:center }
-.btn_act { padding:10px 20px;width:120px;background-color:#1f1f1f;color:white;border:1px solid }
-#loading { margin:0 auto;padding:50px 10px 10px;font-size:16px;text-align:center;color:grey }
-.load_img { height:30px }
-.more_div { background:white;color:grey }
+body,ul,li {
+ padding:0;
+ margin:0;
+ border:0;
+}
+body {
+ font-size:12px;
+ -webkit-user-select:none;
+ -webkit-text-size-adjust:none;
+ font-family:helvetica;
+}
+#header { }
+#header a {
+ color:#f3f3f3;
+ text-decoration:none;
+ font-weight:bold;
+ text-shadow:0 -1px 0 rgba(0,0,0,0.5);
+}
+#footer { }
+#wrapper {
+ /*position:absolute; z-index:1;*/
+ top:45px; bottom:48px;
+ text-align:center;
+ width:100%;
+ min-height:100px;
+ overflow:auto;
+}
+/* ============================================================================= */
+
+#scroller {
+ position:absolute; z-index:1;
+/* -webkit-touch-callout:none;*/
+ -webkit-tap-highlight-color:rgba(0,0,0,0);
+ width:100%;
+ padding:0;
+}
+
+#scroller ul {
+ list-style:none;
+ padding:0;
+ margin:0;
+ width:100%;
+ text-align:left;
+}
+
+#scroller li {
+ padding:0 10px;
+ height:40px;
+ line-height:40px;
+ border-bottom:1px solid #ccc;
+ border-top:1px solid #fff;
+ background-color:#fafafa;
+ font-size:14px;
+}
+
+#myFrame {
+ position:absolute;
+ top:0; left:0;
+}
+.w30p { width:30% }
+/* ============================================================================= */
+
+#outer {
+ width:100%;
+ margin:auto;
+ text-align:center;
+}
+.inner {
+ display: table-cell;
+ vertical-align:middle;
+ text-align:center;
+
+ width:90px;
+ height:136px;
+ margin:5px;
+ border:1px solid #1f1f1f;
+}
+.inner_img {
+ vertical-align:middle;
+ max-width:90px;
+ max-height:90px;
+}
+.inner_title {
+ position:relative;
+ float:left;
+ width:90px;
+ overflow:hidden;
+ background:#1f1f1f;
+ color:#fff;
+ height:40px;
+ padding:3px 0px;
+}
+* html .inner {display:inline} /* for ie*/
+html>body #outer {display:table} /*for mozilla */
+html>body .inner {display:table;float:left} /*for mozilla */
+@media all and (min-width: 0px) { /* opera 7 styles */
+ html>body .inner {display:inline-block;float:none;}
+}
+/* ============================================================================= */
+
+.table { width:100% }
+.nowrap { overflow:hidden;white-space:nowrap }
+.tbl_title { font-size:16px;color:white;text-align:center;background:#1f1f1f; }
+.tpl_title_pre { height:38px;width:20px; background:#D70377;float:left;margin:-10px; }
+.title,a.title { font-size:16px;clolor:black }
+.desc { font-size:14px;color:grey }
+.pl15 { padding-left:15px }
+
+a {
+}
+
+a:link {
+color: black;
+text-decoration: none;
+}
+a:visited {
+color: black;
+text-decoration: none;
+}
+a:hover {
+color: black;
+text-decoration: none;
+}
+a:active {
+color: black;
+text-decoration: none;
+}
+
+/* ============================================================================= */
+
+.fl_left { float:left }
+.fl_right { float:right }
+.pl10 {padding-left:10px}
+.pl20 {padding-left:20px}
+.pt20 {padding-top:10px}
+.clear { clear:both }
+.act { font-size:20px;text-align:center }
+.btn_act { padding:10px 20px;width:120px;background-color:#1f1f1f;color:white;border:1px solid }
+#loading { margin:0 auto;padding:50px 10px 10px;font-size:16px;text-align:center;color:grey }
+.load_img { height:30px }
+.more_div { background:white;color:grey }
diff --git a/qpysdk/src/main/assets/stylesheets/js/bootstrap.min.js b/qpysdk/src/main/assets/stylesheets/js/bootstrap.min.js
index 00c895f0..3d46e4b1 100644
--- a/qpysdk/src/main/assets/stylesheets/js/bootstrap.min.js
+++ b/qpysdk/src/main/assets/stylesheets/js/bootstrap.min.js
@@ -1,7 +1,7 @@
-/*!
- * Bootstrap v4.1.3 (https://getbootstrap.com/)
- * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
- * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- */
-!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,h){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!(Ie={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(Se={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},we="out",Ne={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:(De="show")+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Oe="fade",ke="show",Pe=".tooltip-inner",je=".arrow",He="hover",Le="focus",Re="click",xe="manual",We=function(){function i(t,e){if("undefined"==typeof h)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=pe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(pe(this.getTipElement()).hasClass(ke))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),pe.removeData(this.element,this.constructor.DATA_KEY),pe(this.element).off(this.constructor.EVENT_KEY),pe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&pe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===pe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=pe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){pe(this.element).trigger(t);var n=pe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Fn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&pe(i).addClass(Oe);var o="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,s=this._getAttachment(o);this.addAttachmentClass(s);var a=!1===this.config.container?document.body:pe(document).find(this.config.container);pe(i).data(this.constructor.DATA_KEY,this),pe.contains(this.element.ownerDocument.documentElement,this.tip)||pe(i).appendTo(a),pe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new h(this.element,i,{placement:s,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),pe(i).addClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().on("mouseover",null,pe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,pe(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(pe(this.tip).hasClass(Oe)){var c=Fn.getTransitionDurationFromElement(this.tip);pe(this.tip).one(Fn.TRANSITION_END,l).emulateTransitionEnd(c)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=pe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),pe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(pe(this.element).trigger(i),!i.isDefaultPrevented()){if(pe(n).removeClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().off("mouseover",null,pe.noop),this._activeTrigger[Re]=!1,this._activeTrigger[Le]=!1,this._activeTrigger[He]=!1,pe(this.tip).hasClass(Oe)){var o=Fn.getTransitionDurationFromElement(n);pe(n).one(Fn.TRANSITION_END,r).emulateTransitionEnd(o)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){pe(this.getTipElement()).addClass(Te+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||pe(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(pe(t.querySelectorAll(Pe)),this.getTitle()),pe(t).removeClass(Oe+" "+ke)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?pe(e).parent().is(t)||t.empty().append(e):t.text(pe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return Ie[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)pe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xe){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;pe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}pe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Le:He]=!0),pe(e.getTipElement()).hasClass(ke)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Le:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,pe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Fn.typeCheckConfig(ve,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=pe(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(pe(t).removeClass(Oe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=pe(this).data(ye),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),pe(this).data(ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}},{key:"Default",get:function(){return Ae}},{key:"NAME",get:function(){return ve}},{key:"DATA_KEY",get:function(){return ye}},{key:"Event",get:function(){return Ne}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}(),pe.fn[ve]=We._jQueryInterface,pe.fn[ve].Constructor=We,pe.fn[ve].noConflict=function(){return pe.fn[ve]=Ce,We._jQueryInterface},We),Jn=(qe="popover",Ke="."+(Fe="bs.popover"),Me=(Ue=e).fn[qe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},zn.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},zn.DefaultType,{content:"(string|element|function)"}),ze="fade",Ze=".popover-header",Ge=".popover-body",$e={HIDE:"hide"+Ke,HIDDEN:"hidden"+Ke,SHOW:(Je="show")+Ke,SHOWN:"shown"+Ke,INSERTED:"inserted"+Ke,CLICK:"click"+Ke,FOCUSIN:"focusin"+Ke,FOCUSOUT:"focusout"+Ke,MOUSEENTER:"mouseenter"+Ke,MOUSELEAVE:"mouseleave"+Ke},Xe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){Ue(this.getTipElement()).addClass(Qe+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||Ue(this.config.template)[0],this.tip},r.setContent=function(){var t=Ue(this.getTipElement());this.setElementContent(t.find(Ze),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ge),e),t.removeClass(ze+" "+Je)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=Ue(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t li > .active",xn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',Wn=".dropdown-toggle",Un="> .dropdown-menu .active",qn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&bn(this._element).hasClass(Nn)||bn(this._element).hasClass(On))){var t,i,e=bn(this._element).closest(Hn)[0],r=Fn.getSelectorFromElement(this._element);if(e){var o="UL"===e.nodeName?Rn:Ln;i=(i=bn.makeArray(bn(e).find(o)))[i.length-1]}var s=bn.Event(Dn.HIDE,{relatedTarget:this._element}),a=bn.Event(Dn.SHOW,{relatedTarget:i});if(i&&bn(i).trigger(s),bn(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){r&&(t=document.querySelector(r)),this._activate(this._element,e);var l=function(){var t=bn.Event(Dn.HIDDEN,{relatedTarget:n._element}),e=bn.Event(Dn.SHOWN,{relatedTarget:i});bn(i).trigger(t),bn(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){bn.removeData(this._element,Sn),this._element=null},t._activate=function(t,e,n){var i=this,r=("UL"===e.nodeName?bn(e).find(Rn):bn(e).children(Ln))[0],o=n&&r&&bn(r).hasClass(kn),s=function(){return i._transitionComplete(t,r,n)};if(r&&o){var a=Fn.getTransitionDurationFromElement(r);bn(r).one(Fn.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){bn(e).removeClass(Pn+" "+Nn);var i=bn(e.parentNode).find(Un)[0];i&&bn(i).removeClass(Nn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(bn(t).addClass(Nn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),Fn.reflow(t),bn(t).addClass(Pn),t.parentNode&&bn(t.parentNode).hasClass(wn)){var r=bn(t).closest(jn)[0];if(r){var o=[].slice.call(r.querySelectorAll(Wn));bn(o).addClass(Nn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=bn(this),e=t.data(Sn);if(e||(e=new i(this),t.data(Sn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}}]),i}(),bn(document).on(Dn.CLICK_DATA_API,xn,function(t){t.preventDefault(),qn._jQueryInterface.call(bn(this),"show")}),bn.fn.tab=qn._jQueryInterface,bn.fn.tab.Constructor=qn,bn.fn.tab.noConflict=function(){return bn.fn.tab=An,qn._jQueryInterface},qn);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||4<=e[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=Fn,t.Alert=Kn,t.Button=Mn,t.Carousel=Qn,t.Collapse=Bn,t.Dropdown=Vn,t.Modal=Yn,t.Popover=Jn,t.Scrollspy=Zn,t.Tab=Gn,t.Tooltip=zn,Object.defineProperty(t,"__esModule",{value:!0})});
+/*!
+ * Bootstrap v4.1.3 (https://getbootstrap.com/)
+ * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,h){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!(Ie={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(Se={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},we="out",Ne={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:(De="show")+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Oe="fade",ke="show",Pe=".tooltip-inner",je=".arrow",He="hover",Le="focus",Re="click",xe="manual",We=function(){function i(t,e){if("undefined"==typeof h)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=pe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(pe(this.getTipElement()).hasClass(ke))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),pe.removeData(this.element,this.constructor.DATA_KEY),pe(this.element).off(this.constructor.EVENT_KEY),pe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&pe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===pe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=pe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){pe(this.element).trigger(t);var n=pe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Fn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&pe(i).addClass(Oe);var o="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,s=this._getAttachment(o);this.addAttachmentClass(s);var a=!1===this.config.container?document.body:pe(document).find(this.config.container);pe(i).data(this.constructor.DATA_KEY,this),pe.contains(this.element.ownerDocument.documentElement,this.tip)||pe(i).appendTo(a),pe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new h(this.element,i,{placement:s,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),pe(i).addClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().on("mouseover",null,pe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,pe(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(pe(this.tip).hasClass(Oe)){var c=Fn.getTransitionDurationFromElement(this.tip);pe(this.tip).one(Fn.TRANSITION_END,l).emulateTransitionEnd(c)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=pe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),pe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(pe(this.element).trigger(i),!i.isDefaultPrevented()){if(pe(n).removeClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().off("mouseover",null,pe.noop),this._activeTrigger[Re]=!1,this._activeTrigger[Le]=!1,this._activeTrigger[He]=!1,pe(this.tip).hasClass(Oe)){var o=Fn.getTransitionDurationFromElement(n);pe(n).one(Fn.TRANSITION_END,r).emulateTransitionEnd(o)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){pe(this.getTipElement()).addClass(Te+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||pe(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(pe(t.querySelectorAll(Pe)),this.getTitle()),pe(t).removeClass(Oe+" "+ke)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?pe(e).parent().is(t)||t.empty().append(e):t.text(pe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return Ie[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)pe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xe){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;pe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}pe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Le:He]=!0),pe(e.getTipElement()).hasClass(ke)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Le:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,pe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Fn.typeCheckConfig(ve,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=pe(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(pe(t).removeClass(Oe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=pe(this).data(ye),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),pe(this).data(ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}},{key:"Default",get:function(){return Ae}},{key:"NAME",get:function(){return ve}},{key:"DATA_KEY",get:function(){return ye}},{key:"Event",get:function(){return Ne}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}(),pe.fn[ve]=We._jQueryInterface,pe.fn[ve].Constructor=We,pe.fn[ve].noConflict=function(){return pe.fn[ve]=Ce,We._jQueryInterface},We),Jn=(qe="popover",Ke="."+(Fe="bs.popover"),Me=(Ue=e).fn[qe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},zn.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},zn.DefaultType,{content:"(string|element|function)"}),ze="fade",Ze=".popover-header",Ge=".popover-body",$e={HIDE:"hide"+Ke,HIDDEN:"hidden"+Ke,SHOW:(Je="show")+Ke,SHOWN:"shown"+Ke,INSERTED:"inserted"+Ke,CLICK:"click"+Ke,FOCUSIN:"focusin"+Ke,FOCUSOUT:"focusout"+Ke,MOUSEENTER:"mouseenter"+Ke,MOUSELEAVE:"mouseleave"+Ke},Xe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){Ue(this.getTipElement()).addClass(Qe+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||Ue(this.config.template)[0],this.tip},r.setContent=function(){var t=Ue(this.getTipElement());this.setElementContent(t.find(Ze),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ge),e),t.removeClass(ze+" "+Je)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=Ue(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t li > .active",xn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',Wn=".dropdown-toggle",Un="> .dropdown-menu .active",qn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&bn(this._element).hasClass(Nn)||bn(this._element).hasClass(On))){var t,i,e=bn(this._element).closest(Hn)[0],r=Fn.getSelectorFromElement(this._element);if(e){var o="UL"===e.nodeName?Rn:Ln;i=(i=bn.makeArray(bn(e).find(o)))[i.length-1]}var s=bn.Event(Dn.HIDE,{relatedTarget:this._element}),a=bn.Event(Dn.SHOW,{relatedTarget:i});if(i&&bn(i).trigger(s),bn(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){r&&(t=document.querySelector(r)),this._activate(this._element,e);var l=function(){var t=bn.Event(Dn.HIDDEN,{relatedTarget:n._element}),e=bn.Event(Dn.SHOWN,{relatedTarget:i});bn(i).trigger(t),bn(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){bn.removeData(this._element,Sn),this._element=null},t._activate=function(t,e,n){var i=this,r=("UL"===e.nodeName?bn(e).find(Rn):bn(e).children(Ln))[0],o=n&&r&&bn(r).hasClass(kn),s=function(){return i._transitionComplete(t,r,n)};if(r&&o){var a=Fn.getTransitionDurationFromElement(r);bn(r).one(Fn.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){bn(e).removeClass(Pn+" "+Nn);var i=bn(e.parentNode).find(Un)[0];i&&bn(i).removeClass(Nn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(bn(t).addClass(Nn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),Fn.reflow(t),bn(t).addClass(Pn),t.parentNode&&bn(t.parentNode).hasClass(wn)){var r=bn(t).closest(jn)[0];if(r){var o=[].slice.call(r.querySelectorAll(Wn));bn(o).addClass(Nn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=bn(this),e=t.data(Sn);if(e||(e=new i(this),t.data(Sn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}}]),i}(),bn(document).on(Dn.CLICK_DATA_API,xn,function(t){t.preventDefault(),qn._jQueryInterface.call(bn(this),"show")}),bn.fn.tab=qn._jQueryInterface,bn.fn.tab.Constructor=qn,bn.fn.tab.noConflict=function(){return bn.fn.tab=An,qn._jQueryInterface},qn);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||4<=e[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=Fn,t.Alert=Kn,t.Button=Mn,t.Carousel=Qn,t.Collapse=Bn,t.Dropdown=Vn,t.Modal=Yn,t.Popover=Jn,t.Scrollspy=Zn,t.Tab=Gn,t.Tooltip=zn,Object.defineProperty(t,"__esModule",{value:!0})});
//# sourceMappingURL=bootstrap.min.js.map
\ No newline at end of file
diff --git a/qpysdk/src/main/assets/stylesheets/js/jquery.min.js b/qpysdk/src/main/assets/stylesheets/js/jquery.min.js
index a1c07fd8..409c3f49 100644
--- a/qpysdk/src/main/assets/stylesheets/js/jquery.min.js
+++ b/qpysdk/src/main/assets/stylesheets/js/jquery.min.js
@@ -1,2 +1,2 @@
-/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML=" ";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/
-
- {{QPYBUILTIN}}
-
-
-
-
-
-
-
-
- {{WEBAPPCONTENT}}
-
-
-
-
-