From e35b368d50ca8cbe98f440c640cb3d4facff153a Mon Sep 17 00:00:00 2001 From: Anthony Restaino Date: Sun, 19 Jul 2015 15:36:41 -0400 Subject: [PATCH] Updated ProxyUtils to automatically start TOR when needed, more abstraction of BrowserActivity, other cleanup --- app/app.iml | 2 + app/build.gradle | 1 + .../browser/lightning/utils/ProxyUtils.java | 2 +- .../lightning/activity/BrowserActivity.java | 45 +-- .../lightning/activity/IncognitoActivity.java | 1 - .../lightning/activity/MainActivity.java | 1 - .../fragment/DisplaySettingsFragment.java | 2 +- .../acr/browser/lightning/utils/Utils.java | 377 +++++++++--------- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/xml/preference_about.xml | 74 ++-- 10 files changed, 270 insertions(+), 239 deletions(-) diff --git a/app/app.iml b/app/app.iml index a2c8d24..3d6f837 100644 --- a/app/app.iml +++ b/app/app.iml @@ -93,6 +93,7 @@ + @@ -115,6 +116,7 @@ + diff --git a/app/build.gradle b/app/build.gradle index b18e477..6afc2c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,6 +46,7 @@ android { dependencies { compile 'com.android.support:palette-v7:22.2.0' compile 'com.android.support:appcompat-v7:22.2.0' + compile 'com.android.support:design:22.2.0' compile 'org.jsoup:jsoup:1.8.1' // Only Lightning Plus needs the proxy libraries diff --git a/app/src/LightningPlus/java/acr/browser/lightning/utils/ProxyUtils.java b/app/src/LightningPlus/java/acr/browser/lightning/utils/ProxyUtils.java index bea7a92..18cbd5a 100644 --- a/app/src/LightningPlus/java/acr/browser/lightning/utils/ProxyUtils.java +++ b/app/src/LightningPlus/java/acr/browser/lightning/utils/ProxyUtils.java @@ -117,7 +117,7 @@ public class ProxyUtils { case Constants.PROXY_ORBOT: if (!OrbotHelper.isOrbotRunning(activity)) - OrbotHelper.requestShowOrbotStart(activity); + OrbotHelper.requestStartTor(activity); host = "localhost"; port = 8118; break; diff --git a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java index f7194a3..debe836 100644 --- a/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/BrowserActivity.java @@ -139,7 +139,7 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse // List private final List mWebViewList = new ArrayList<>(); - private List mBookmarkList; + private final List mBookmarkList = new ArrayList<>(); private LightningView mCurrentView; private WebView mWebView; @@ -164,8 +164,7 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse private Activity mActivity; // Primatives - private boolean mSystemBrowser = false, mIsNewIntent = false, mFullScreen, mColorMode, - mDarkTheme; + private boolean mSystemBrowser = false, mIsNewIntent = false, mFullScreen, mColorMode, mDarkTheme; private int mOriginalOrientation, mBackgroundColor, mIdGenerator; private String mSearchText, mUntitledTitle, mHomepage, mCameraPhotoPath; @@ -200,6 +199,8 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse public abstract void updateHistory(final String title, final String url); + abstract void updateCookiePreference(); + @Override protected void onCreate(Bundle savedInstanceState) { @@ -318,7 +319,8 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse @Override public void run() { mBookmarkManager = BookmarkManager.getInstance(mActivity.getApplicationContext()); - mBookmarkList = mBookmarkManager.getBookmarks(true); + mBookmarkList.clear(); + mBookmarkList.addAll(mBookmarkManager.getBookmarks(true)); if (mBookmarkList.size() == 0 && mPreferences.getDefaultBookmarks()) { for (String[] array : BookmarkManager.DEFAULT_BOOKMARKS) { HistoryItem bookmark = new HistoryItem(array[0], array[1]); @@ -715,13 +717,6 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse mProxyUtils.updateProxySettings(this); } - /* - * Override this if class overrides BrowserActivity - */ - void updateCookiePreference() { - - } - @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER) { @@ -803,7 +798,7 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse if (mCurrentView != null && !mCurrentView.getUrl().startsWith(Constants.FILE)) { HistoryItem bookmark = new HistoryItem(mCurrentView.getUrl(), mCurrentView.getTitle()); - if (mBookmarkManager.addBookmark(bookmark) && mBookmarkList != null) { + if (mBookmarkManager.addBookmark(bookmark)) { mBookmarkList.add(bookmark); Collections.sort(mBookmarkList, new SortIgnoreCase()); notifyBookmarkDataSetChanged(); @@ -829,8 +824,6 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse * adapter doesn't always change when notifyDataChanged gets called. */ private void notifyBookmarkDataSetChanged() { - mBookmarkAdapter.clear(); - mBookmarkAdapter.addAll(mBookmarkList); mBookmarkAdapter.notifyDataSetChanged(); } @@ -1111,8 +1104,10 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse url = intent.getDataString(); } int num = 0; + String source = null; if (intent != null && intent.getExtras() != null) { num = intent.getExtras().getInt(getPackageName() + ".Origin"); + source = intent.getExtras().getString("SOURCE"); } if (num == 1) { mCurrentView.loadUrl(url); @@ -1122,7 +1117,7 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse url = null; } newTab(url, true); - mIsNewIntent = true; + mIsNewIntent = (source == null); } } @@ -1403,19 +1398,17 @@ public abstract class BrowserActivity extends ThemableActivity implements Browse if (mCurrentView != null) { mCurrentView.resumeTimers(); mCurrentView.onResume(); - - mHistoryDatabase = HistoryDatabase.getInstance(getApplicationContext()); - mBookmarkList = mBookmarkManager.getBookmarks(true); - notifyBookmarkDataSetChanged(); } + mHistoryDatabase = HistoryDatabase.getInstance(getApplicationContext()); + mBookmarkList.clear(); + mBookmarkList.addAll(mBookmarkManager.getBookmarks(true)); + notifyBookmarkDataSetChanged(); initializePreferences(); - if (mWebViewList != null) { - for (int n = 0; n < mWebViewList.size(); n++) { - if (mWebViewList.get(n) != null) { - mWebViewList.get(n).initializePreferences(this); - } else { - mWebViewList.remove(n); - } + for (int n = 0; n < mWebViewList.size(); n++) { + if (mWebViewList.get(n) != null) { + mWebViewList.get(n).initializePreferences(this); + } else { + mWebViewList.remove(n); } } diff --git a/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java b/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java index a62f321..9b59c76 100644 --- a/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/IncognitoActivity.java @@ -20,7 +20,6 @@ public class IncognitoActivity extends BrowserActivity { CookieSyncManager.createInstance(this); } cookieManager.setAcceptCookie(PreferenceManager.getInstance().getIncognitoCookiesEnabled()); - super.updateCookiePreference(); } @Override diff --git a/app/src/main/java/acr/browser/lightning/activity/MainActivity.java b/app/src/main/java/acr/browser/lightning/activity/MainActivity.java index e7db0df..2c08b49 100644 --- a/app/src/main/java/acr/browser/lightning/activity/MainActivity.java +++ b/app/src/main/java/acr/browser/lightning/activity/MainActivity.java @@ -20,7 +20,6 @@ public class MainActivity extends BrowserActivity { CookieSyncManager.createInstance(this); } cookieManager.setAcceptCookie(PreferenceManager.getInstance().getCookiesEnabled()); - super.updateCookiePreference(); } @Override diff --git a/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java b/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java index 835cf4f..bc06e04 100644 --- a/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java +++ b/app/src/main/java/acr/browser/lightning/fragment/DisplaySettingsFragment.java @@ -143,7 +143,7 @@ public class DisplaySettingsFragment extends PreferenceFragment implements Prefe private void themePicker() { AlertDialog.Builder picker = new AlertDialog.Builder(mActivity); - picker.setTitle(getResources().getString(R.string.url_contents)); + picker.setTitle(getResources().getString(R.string.theme)); int n = mPreferences.getUseTheme(); picker.setSingleChoiceItems(mThemeOptions, n, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/acr/browser/lightning/utils/Utils.java b/app/src/main/java/acr/browser/lightning/utils/Utils.java index 4d6ad13..06eb6ac 100644 --- a/app/src/main/java/acr/browser/lightning/utils/Utils.java +++ b/app/src/main/java/acr/browser/lightning/utils/Utils.java @@ -17,9 +17,11 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.os.Environment; import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; import android.support.v7.app.AlertDialog; import android.util.DisplayMetrics; import android.util.Log; +import android.view.View; import android.webkit.URLUtil; import android.widget.Toast; @@ -37,139 +39,146 @@ import acr.browser.lightning.download.DownloadHandler; public final class Utils { - private Utils() { - } + private Utils() { + } - public static void downloadFile(final Activity activity, final String url, + public static void downloadFile(final Activity activity, final String url, final String userAgent, final String contentDisposition) { - String fileName = URLUtil.guessFileName(url, null, null); - DownloadHandler.onDownloadStart(activity, url, userAgent, contentDisposition, null + String fileName = URLUtil.guessFileName(url, null, null); + DownloadHandler.onDownloadStart(activity, url, userAgent, contentDisposition, null ); - Log.i(Constants.TAG, "Downloading" + fileName); - } + Log.i(Constants.TAG, "Downloading" + fileName); + } - public static Intent newEmailIntent(String address, String subject, + public static Intent newEmailIntent(String address, String subject, String body, String cc) { - Intent intent = new Intent(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_EMAIL, new String[] { address }); - intent.putExtra(Intent.EXTRA_TEXT, body); - intent.putExtra(Intent.EXTRA_SUBJECT, subject); - intent.putExtra(Intent.EXTRA_CC, cc); - intent.setType("message/rfc822"); - return intent; - } - - public static void createInformativeDialog(Context context, String title, String message) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(title); - builder.setMessage(message) - .setCancelable(true) - .setPositiveButton(context.getResources().getString(R.string.action_ok), + Intent intent = new Intent(Intent.ACTION_SEND); + intent.putExtra(Intent.EXTRA_EMAIL, new String[]{address}); + intent.putExtra(Intent.EXTRA_TEXT, body); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_CC, cc); + intent.setType("message/rfc822"); + return intent; + } + + public static void createInformativeDialog(Context context, String title, String message) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(title); + builder.setMessage(message) + .setCancelable(true) + .setPositiveButton(context.getResources().getString(R.string.action_ok), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { } }); - AlertDialog alert = builder.create(); - alert.show(); - } + AlertDialog alert = builder.create(); + alert.show(); + } - public static void showToast(Context context, String message) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); - } + public static void showToast(Context context, String message) { + Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); + } public static void showToast(Context context, @StringRes int resource) { Toast.makeText(context, resource, Toast.LENGTH_SHORT).show(); } - /** - * Returns the number of pixels corresponding to the passed density pixels - */ - public static int convertDpToPixels(int dp) { - DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); - return (int) (dp * metrics.density + 0.5f); - } - - public static String getDomainName(String url) { - boolean ssl = url.startsWith(Constants.HTTPS); - int index = url.indexOf('/', 8); - if (index != -1) { - url = url.substring(0, index); - } - - URI uri; - String domain = null; - try { - uri = new URI(url); - domain = uri.getHost(); - } catch (URISyntaxException e) { - e.printStackTrace(); - } - - if (domain == null || domain.isEmpty()) { - return url; - } - if (ssl) - return Constants.HTTPS + domain; - else - return domain.startsWith("www.") ? domain.substring(4) : domain; - } - - public static String getProtocol(String url) { - int index = url.indexOf('/'); - return url.substring(0, index + 2); - } - - public static String[] getArray(String input) { - return input.split(Constants.SEPARATOR); - } - - public static void trimCache(Context context) { - try { - File dir = context.getCacheDir(); - - if (dir != null && dir.isDirectory()) { - deleteDir(dir); - } - } catch (Exception ignored) { - - } - } - - public static boolean deleteDir(File dir) { - if (dir != null && dir.isDirectory()) { - String[] children = dir.list(); - for (String aChildren : children) { - boolean success = deleteDir(new File(dir, aChildren)); - if (!success) { - return false; - } - } - } - // The directory is now empty so delete it - return dir != null && dir.delete(); - } - - /** - * Creates and returns a new favicon which is the same as the provided - * favicon but with horizontal or vertical padding of 4dp - * - * @param bitmap - * is the bitmap to pad. - * @return the padded bitmap. - */ - public static Bitmap padFavicon(Bitmap bitmap) { - int padding = Utils.convertDpToPixels(4); - - Bitmap paddedBitmap = Bitmap.createBitmap(bitmap.getWidth() + padding, bitmap.getHeight() - + padding, Bitmap.Config.ARGB_8888); - - Canvas canvas = new Canvas(paddedBitmap); - canvas.drawARGB(0x00, 0x00, 0x00, 0x00); // this represents white color - canvas.drawBitmap(bitmap, padding / 2, padding / 2, new Paint(Paint.FILTER_BITMAP_FLAG)); - - return paddedBitmap; - } + public static void showSnackBar(View view, String message) { + Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show(); + } + + public static void showSnackBar(View view, @StringRes int resource) { + Snackbar.make(view, resource, Snackbar.LENGTH_SHORT).show(); + } + + /** + * Returns the number of pixels corresponding to the passed density pixels + */ + public static int convertDpToPixels(int dp) { + DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + return (int) (dp * metrics.density + 0.5f); + } + + public static String getDomainName(String url) { + boolean ssl = url.startsWith(Constants.HTTPS); + int index = url.indexOf('/', 8); + if (index != -1) { + url = url.substring(0, index); + } + + URI uri; + String domain = null; + try { + uri = new URI(url); + domain = uri.getHost(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + + if (domain == null || domain.isEmpty()) { + return url; + } + if (ssl) + return Constants.HTTPS + domain; + else + return domain.startsWith("www.") ? domain.substring(4) : domain; + } + + public static String getProtocol(String url) { + int index = url.indexOf('/'); + return url.substring(0, index + 2); + } + + public static String[] getArray(String input) { + return input.split(Constants.SEPARATOR); + } + + public static void trimCache(Context context) { + try { + File dir = context.getCacheDir(); + + if (dir != null && dir.isDirectory()) { + deleteDir(dir); + } + } catch (Exception ignored) { + + } + } + + public static boolean deleteDir(File dir) { + if (dir != null && dir.isDirectory()) { + String[] children = dir.list(); + for (String aChildren : children) { + boolean success = deleteDir(new File(dir, aChildren)); + if (!success) { + return false; + } + } + } + // The directory is now empty so delete it + return dir != null && dir.delete(); + } + + /** + * Creates and returns a new favicon which is the same as the provided + * favicon but with horizontal or vertical padding of 4dp + * + * @param bitmap is the bitmap to pad. + * @return the padded bitmap. + */ + public static Bitmap padFavicon(Bitmap bitmap) { + int padding = Utils.convertDpToPixels(4); + + Bitmap paddedBitmap = Bitmap.createBitmap(bitmap.getWidth() + padding, bitmap.getHeight() + + padding, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(paddedBitmap); + canvas.drawARGB(0x00, 0x00, 0x00, 0x00); // this represents white color + canvas.drawBitmap(bitmap, padding / 2, padding / 2, new Paint(Paint.FILTER_BITMAP_FLAG)); + + return paddedBitmap; + } public static boolean isColorTooDark(int color) { final byte RED_CHANNEL = 16; @@ -185,71 +194,71 @@ public final class Utils { return gray < 0x727272; } - public static int mixTwoColors(int color1, int color2, float amount) { - final byte ALPHA_CHANNEL = 24; - final byte RED_CHANNEL = 16; - final byte GREEN_CHANNEL = 8; - //final byte BLUE_CHANNEL = 0; - - final float inverseAmount = 1.0f - amount; - - int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff; - int b = ((int) (((float) (color1 & 0xff) * amount) + ((float) (color2 & 0xff) * inverseAmount))) & 0xff; - - return 0xff << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b; - } - - @SuppressLint("SimpleDateFormat") - public static File createImageFile() throws IOException { - // Create an image file name - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); - String imageFileName = "JPEG_" + timeStamp + "_"; - File storageDir = Environment - .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); - return File.createTempFile(imageFileName, /* prefix */ - ".jpg", /* suffix */ - storageDir /* directory */ - ); - } - - public static boolean isFlashInstalled(Context context) { - try { - PackageManager pm = context.getPackageManager(); - ApplicationInfo ai = pm.getApplicationInfo("com.adobe.flashplayer", 0); - if (ai != null) { - return true; - } - } catch (PackageManager.NameNotFoundException e) { - return false; - } - return false; - } - - public static Bitmap getWebpageBitmap(Resources resources, boolean dark) { - if (dark) { - if (mWebIconDark == null) { - mWebIconDark = BitmapFactory.decodeResource(resources, R.drawable.ic_webpage_dark); - } - return mWebIconDark; - } else { - if (mWebIconLight == null) { - mWebIconLight = BitmapFactory.decodeResource(resources, R.drawable.ic_webpage); - } - return mWebIconLight; - } - } - - private static Bitmap mWebIconLight; - private static Bitmap mWebIconDark; - - public static void close(Closeable closeable) { - if (closeable == null) - return; - try { - closeable.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } + public static int mixTwoColors(int color1, int color2, float amount) { + final byte ALPHA_CHANNEL = 24; + final byte RED_CHANNEL = 16; + final byte GREEN_CHANNEL = 8; + //final byte BLUE_CHANNEL = 0; + + final float inverseAmount = 1.0f - amount; + + int r = ((int) (((float) (color1 >> RED_CHANNEL & 0xff) * amount) + ((float) (color2 >> RED_CHANNEL & 0xff) * inverseAmount))) & 0xff; + int g = ((int) (((float) (color1 >> GREEN_CHANNEL & 0xff) * amount) + ((float) (color2 >> GREEN_CHANNEL & 0xff) * inverseAmount))) & 0xff; + int b = ((int) (((float) (color1 & 0xff) * amount) + ((float) (color2 & 0xff) * inverseAmount))) & 0xff; + + return 0xff << ALPHA_CHANNEL | r << RED_CHANNEL | g << GREEN_CHANNEL | b; + } + + @SuppressLint("SimpleDateFormat") + public static File createImageFile() throws IOException { + // Create an image file name + String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); + String imageFileName = "JPEG_" + timeStamp + "_"; + File storageDir = Environment + .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + return File.createTempFile(imageFileName, /* prefix */ + ".jpg", /* suffix */ + storageDir /* directory */ + ); + } + + public static boolean isFlashInstalled(Context context) { + try { + PackageManager pm = context.getPackageManager(); + ApplicationInfo ai = pm.getApplicationInfo("com.adobe.flashplayer", 0); + if (ai != null) { + return true; + } + } catch (PackageManager.NameNotFoundException e) { + return false; + } + return false; + } + + public static Bitmap getWebpageBitmap(Resources resources, boolean dark) { + if (dark) { + if (mWebIconDark == null) { + mWebIconDark = BitmapFactory.decodeResource(resources, R.drawable.ic_webpage_dark); + } + return mWebIconDark; + } else { + if (mWebIconLight == null) { + mWebIconLight = BitmapFactory.decodeResource(resources, R.drawable.ic_webpage); + } + return mWebIconLight; + } + } + + private static Bitmap mWebIconLight; + private static Bitmap mWebIconDark; + + public static void close(Closeable closeable) { + if (closeable == null) + return; + try { + closeable.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1f663e4..768a4ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,7 +53,7 @@ No stock browser detected Supported stock browser detected Hide status bar while browsing - Clear browser cookies + Clear Browser Cookies What would you like to do with this image? Download Open @@ -110,7 +110,7 @@ Auto Contact Me twitter.com/RestainoAnthony - Clear cache + Clear Cache Cache Cleared Bookmarks Were Imported History Cleared diff --git a/app/src/main/res/xml/preference_about.xml b/app/src/main/res/xml/preference_about.xml index 9843e1d..cbff816 100644 --- a/app/src/main/res/xml/preference_about.xml +++ b/app/src/main/res/xml/preference_about.xml @@ -3,58 +3,86 @@ + android:summary="@string/url_twitter" + android:title="@string/action_follow_me"> + android:data="http://twitter.com/RestainoAnthony"> + + + android:key="pref_version" + android:title="@string/version" /> + android:summary="@string/mpl_license" + android:title="@string/app_name"> + android:data="http://www.mozilla.org/MPL/2.0/"> + + + android:summary="@string/apache" + android:title="@string/android_open_source_project"> + android:data="http://www.apache.org/licenses/LICENSE-2.0"> + + + android:summary="@string/freeware" + android:title="@string/hphosts_ad_server_list"> + android:data="http://hosts-file.net/"> + + + android:summary="@string/license_gnu" + android:title="@string/library_netcipher"> + android:data="http://www.gnu.org/licenses/lgpl.html"> + + + android:summary="@string/apache" + android:title="@string/snacktory"> + android:data="http://www.apache.org/licenses/LICENSE-2.0"> + + + android:summary="@string/mit_license" + android:title="@string/jsoup"> + android:data="http://jsoup.org/license"> + + \ No newline at end of file