0

Я пытаюсь создать приложение для своего форума, чтобы пользователи не могли пользоваться браузером на своем мобильном устройстве или использовать другое приложение. Сейчас я сталкиваюсь с проблемой сбоя приложения, но не знаю, почему. Я новичок в этом, так что действительно не знаю, что искать, когда дело доходит до ошибок в коде, может быть, я смогу немного помочь выяснить, что я делаю неправильно?Android WebView add Uploading Download Copy and Paste and Cookie Manager

Возможно, мне нужно что-то изменить в коде, поэтому не стесняйтесь, дайте мне знать, но покажите мне, что нужно будет изменить, пожалуйста. Мне также нужно выяснить, как добавить в приложение функцию копирования и вставки, а также возможность настроить приложение cookie для «помнить меня».

package com.technologx.technologx; 

import android.app.Activity; 
import android.app.AlertDialog; 
import android.app.DownloadManager; 
import android.content.ActivityNotFoundException; 
import android.content.Context; 
import android.content.DialogInterface; 
import android.content.Intent; 
import android.net.Uri; 
import android.os.Bundle; 
import android.os.Environment; 
import android.provider.MediaStore; 
import android.view.View; 
import android.view.Window; 
import android.webkit.CookieSyncManager; 
import android.webkit.JsResult; 
import android.webkit.ValueCallback; 
import android.webkit.WebChromeClient; 
import android.webkit.WebSettings; 
import android.webkit.WebSettings.PluginState; 
import android.webkit.WebView; 
import android.webkit.WebViewClient; 
import android.widget.Toast; 

import java.io.File; 
import java.lang.reflect.Method; 
import java.net.URL; 

public class MainActivity extends Activity { 

    private WebView webView; 

    public void onCreate(Bundle savedInstanceState) { 

     super.onCreate(savedInstanceState); 


     // Adds Progress Bar Support 
     this.getWindow().requestFeature(Window.FEATURE_PROGRESS); 
     // Makes Progress Bar Visible 
     getWindow().setFeatureInt(Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON); 

     // Use forum.xml as webview layout 
     setContentView(R.layout.activity_main); 

     webView.setWebViewClient(new WebViewClient()); 

     webView = (WebView) findViewById(R.id.activity_main_webview); 
     webView.getSettings().setJavaScriptEnabled(true); 

     // Adds Zoom Control (You may not need this) 
     webView.getSettings().setSupportZoom(true); 

     // Enables Multi-Touch. if supported by ROM 
     webView.getSettings().setBuiltInZoomControls(true); 

     initWebView(webView); 
     webView.loadUrl("https://technologx.com"); // TODO input your url 

     // This will handle downloading. It requires Gingerbread, though 
     final DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE); 

     // This is where downloaded files will be written, using the package name isn't required 
     // but it's a good way to communicate who owns the directory 
     final File destinationDir = new File (Environment.getExternalStorageDirectory(), getPackageName()); 
     if (!destinationDir.exists()) { 
      destinationDir.mkdir(); // Don't forget to make the directory if it's not there 
     } 
     webView.setWebViewClient(new WebViewClient() { 
      @Override 
      public boolean shouldOverrideUrlLoading (WebView view, String url) { 
       boolean shouldOverride = false; 
       // We only want to handle requests for mp3 files, everything else the webview 
       // can handle normally 
       if (url.endsWith(".zip")) { 
        shouldOverride = true; 
        Uri source = Uri.parse(url); 

        // Make a new request pointing to the mp3 url 
        DownloadManager.Request request = new DownloadManager.Request(source); 
        // Use the same file name for the destination 
        File destinationFile = new File (destinationDir, source.getLastPathSegment()); 
        request.setDestinationUri(Uri.fromFile(destinationFile)); 
        // Add it to the manager 
        manager.enqueue(request); 
       } 
       return shouldOverride; 
      } 
     }); 

    } 

    private final static Object methodInvoke(Object obj, String method, Class<?>[] parameterTypes, Object[] args) { 
     try { 
      Method m = obj.getClass().getMethod(method, new Class[] { boolean.class }); 
      m.invoke(obj, args); 
     } catch (Exception e) { 
      e.printStackTrace(); 
     } 

     return null; 
    } 

    private void initWebView(WebView webView) { 

     WebSettings settings = webView.getSettings(); 

     settings.setJavaScriptEnabled(true); 
     settings.setAllowFileAccess(true); 
     settings.setDomStorageEnabled(true); 
     settings.setCacheMode(WebSettings.LOAD_NO_CACHE); 
     settings.setLoadWithOverviewMode(true); 
     settings.setUseWideViewPort(true); 
     settings.setSupportZoom(true); 
     // settings.setPluginsEnabled(true); 
     methodInvoke(settings, "setPluginsEnabled", new Class[] { boolean.class }, new Object[] { true }); 
     // settings.setPluginState(PluginState.ON); 
     methodInvoke(settings, "setPluginState", new Class[] { PluginState.class }, new Object[] { PluginState.ON }); 
     // settings.setPluginsEnabled(true); 
     methodInvoke(settings, "setPluginsEnabled", new Class[] { boolean.class }, new Object[] { true }); 
     // settings.setAllowUniversalAccessFromFileURLs(true); 
     methodInvoke(settings, "setAllowUniversalAccessFromFileURLs", new Class[] { boolean.class }, new Object[] { true }); 
     // settings.setAllowFileAccessFromFileURLs(true); 
     methodInvoke(settings, "setAllowFileAccessFromFileURLs", new Class[] { boolean.class }, new Object[] { true }); 

     webView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY); 
     webView.clearHistory(); 
     webView.clearFormData(); 
     webView.clearCache(true); 

     webView.setWebChromeClient(new MyWebChromeClient()); 
     // webView.setDownloadListener(downloadListener); 
    } 

    UploadHandler mUploadHandler; 

    @Override 
    protected void onActivityResult(int requestCode, int resultCode, Intent intent) { 

     if (requestCode == Controller.FILE_SELECTED) { 
      // Chose a file from the file picker. 
      if (mUploadHandler != null) { 
       mUploadHandler.onResult(resultCode, intent); 
      } 
     } 

     super.onActivityResult(requestCode, resultCode, intent); 
    } 

    class MyWebChromeClient extends WebChromeClient { 
     public MyWebChromeClient() { 

     } 

     private String getTitleFromUrl(String url) { 
      String title = url; 
      try { 
       URL urlObj = new URL(url); 
       String host = urlObj.getHost(); 
       if (host != null && !host.isEmpty()) { 
        return urlObj.getProtocol() + "://" + host; 
       } 
       if (url.startsWith("file:")) { 
        String fileName = urlObj.getFile(); 
        if (fileName != null && !fileName.isEmpty()) { 
         return fileName; 
        } 
       } 
      } catch (Exception e) { 
       // ignore 
      } 

      return title; 
     } 

     @Override 
     public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { 
      String newTitle = getTitleFromUrl(url); 

      new AlertDialog.Builder(MainActivity.this).setTitle(newTitle).setMessage(message).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 

       @Override 
       public void onClick(DialogInterface dialog, int which) { 
        result.confirm(); 
       } 
      }).setCancelable(false).create().show(); 
      return true; 
      // return super.onJsAlert(view, url, message, result); 
     } 

     @Override 
     public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { 

      String newTitle = getTitleFromUrl(url); 

      new AlertDialog.Builder(MainActivity.this).setTitle(newTitle).setMessage(message).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 

       @Override 
       public void onClick(DialogInterface dialog, int which) { 
        result.confirm(); 
       } 
      }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 
       public void onClick(DialogInterface dialog, int which) { 
        result.cancel(); 
       } 
      }).setCancelable(false).create().show(); 
      return true; 

      // return super.onJsConfirm(view, url, message, result); 
     } 

     // Android 2.x 
     public void openFileChooser(ValueCallback<Uri> uploadMsg) { 
      openFileChooser(uploadMsg, ""); 
     } 

     // Android 3.0 
     public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) { 
      openFileChooser(uploadMsg, "", "filesystem"); 
     } 

     // Android 4.1 
     public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { 
      mUploadHandler = new UploadHandler(new Controller()); 
      mUploadHandler.openFileChooser(uploadMsg, acceptType, capture); 
     } 

     // Android 4.4, 4.4.1, 4.4.2 
     // openFileChooser function is not called on Android 4.4, 4.4.1, 4.4.2, 
     // you may use your own java script interface or other hybrid framework. 

     // Android 5.0.1 
     public boolean onShowFileChooser(
       WebView webView, ValueCallback<Uri[]> filePathCallback, 
       FileChooserParams fileChooserParams) { 

      String acceptTypes[] = fileChooserParams.getAcceptTypes(); 

      String acceptType = ""; 
      for (int i = 0; i < acceptTypes.length; ++ i) { 
       if (acceptTypes[i] != null && acceptTypes[i].length() != 0) 
        acceptType += acceptTypes[i] + ";"; 
      } 
      if (acceptType.length() == 0) 
       acceptType = "*/*"; 

      final ValueCallback<Uri[]> finalFilePathCallback = filePathCallback; 

      ValueCallback<Uri> vc = new ValueCallback<Uri>() { 

       @Override 
       public void onReceiveValue(Uri value) { 

        Uri[] result; 
        if (value != null) 
         result = new Uri[]{value}; 
        else 
         result = null; 

        finalFilePathCallback.onReceiveValue(result); 

       } 
      }; 

      openFileChooser(vc, acceptType, "filesystem"); 


      return true; 
     } 
    }; 

    class Controller { 
     final static int FILE_SELECTED = 4; 

     Activity getActivity() { 
      return MainActivity.this; 
     } 
    } 


    // public class UploadHandler { 

    class UploadHandler { 
     /* 
     * The Object used to inform the WebView of the file to upload. 
     */ 
     private ValueCallback<Uri> mUploadMessage; 
     private String mCameraFilePath; 
     private boolean mHandled; 
     private boolean mCaughtActivityNotFoundException; 
     private Controller mController; 
     public UploadHandler(Controller controller) { 
      mController = controller; 
     } 
     String getFilePath() { 
      return mCameraFilePath; 
     } 
     boolean handled() { 
      return mHandled; 
     } 
     void onResult(int resultCode, Intent intent) { 
      if (resultCode == Activity.RESULT_CANCELED && mCaughtActivityNotFoundException) { 
       // Couldn't resolve an activity, we are going to try again so skip 
       // this result. 
       mCaughtActivityNotFoundException = false; 
       return; 
      } 
      Uri result = intent == null || resultCode != Activity.RESULT_OK ? null 
        : intent.getData(); 
      // As we ask the camera to save the result of the user taking 
      // a picture, the camera application does not return anything other 
      // than RESULT_OK. So we need to check whether the file we expected 
      // was written to disk in the in the case that we 
      // did not get an intent returned but did get a RESULT_OK. If it was, 
      // we assume that this result has came back from the camera. 
      if (result == null && intent == null && resultCode == Activity.RESULT_OK) { 
       File cameraFile = new File(mCameraFilePath); 
       if (cameraFile.exists()) { 
        result = Uri.fromFile(cameraFile); 
        // Broadcast to the media scanner that we have a new photo 
        // so it will be added into the gallery for the user. 
        mController.getActivity().sendBroadcast(
          new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result)); 
       } 
      } 
      mUploadMessage.onReceiveValue(result); 
      mHandled = true; 
      mCaughtActivityNotFoundException = false; 
     } 
     void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { 
      final String imageMimeType = "image/*"; 
      final String videoMimeType = "video/*"; 
      final String audioMimeType = "audio/*"; 
      final String mediaSourceKey = "capture"; 
      final String mediaSourceValueCamera = "camera"; 
      final String mediaSourceValueFileSystem = "filesystem"; 
      final String mediaSourceValueCamcorder = "camcorder"; 
      final String mediaSourceValueMicrophone = "microphone"; 
      // According to the spec, media source can be 'filesystem' or 'camera' or 'camcorder' 
      // or 'microphone' and the default value should be 'filesystem'. 
      String mediaSource = mediaSourceValueFileSystem; 
      if (mUploadMessage != null) { 
       // Already a file picker operation in progress. 
       return; 
      } 
      mUploadMessage = uploadMsg; 
      // Parse the accept type. 
      String params[] = acceptType.split(";"); 
      String mimeType = params[0]; 
      if (capture.length() > 0) { 
       mediaSource = capture; 
      } 
      if (capture.equals(mediaSourceValueFileSystem)) { 
       // To maintain backwards compatibility with the previous implementation 
       // of the media capture API, if the value of the 'capture' attribute is 
       // "filesystem", we should examine the accept-type for a MIME type that 
       // may specify a different capture value. 
       for (String p : params) { 
        String[] keyValue = p.split("="); 
        if (keyValue.length == 2) { 
         // Process key=value parameters. 
         if (mediaSourceKey.equals(keyValue[0])) { 
          mediaSource = keyValue[1]; 
         } 
        } 
       } 
      } 
      //Ensure it is not still set from a previous upload. 
      mCameraFilePath = null; 
      if (mimeType.equals(imageMimeType)) { 
       if (mediaSource.equals(mediaSourceValueCamera)) { 
        // Specified 'image/*' and requested the camera, so go ahead and launch the 
        // camera directly. 
        startActivity(createCameraIntent()); 
        return; 
       } else { 
        // Specified just 'image/*', capture=filesystem, or an invalid capture parameter. 
        // In all these cases we show a traditional picker filetered on accept type 
        // so launch an intent for both the Camera and image/* OPENABLE. 
        Intent chooser = createChooserIntent(createCameraIntent()); 
        chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(imageMimeType)); 
        startActivity(chooser); 
        return; 
       } 
      } else if (mimeType.equals(videoMimeType)) { 
       if (mediaSource.equals(mediaSourceValueCamcorder)) { 
        // Specified 'video/*' and requested the camcorder, so go ahead and launch the 
        // camcorder directly. 
        startActivity(createCamcorderIntent()); 
        return; 
       } else { 
        // Specified just 'video/*', capture=filesystem or an invalid capture parameter. 
        // In all these cases we show an intent for the traditional file picker, filtered 
        // on accept type so launch an intent for both camcorder and video/* OPENABLE. 
        Intent chooser = createChooserIntent(createCamcorderIntent()); 
        chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(videoMimeType)); 
        startActivity(chooser); 
        return; 
       } 
      } else if (mimeType.equals(audioMimeType)) { 
       if (mediaSource.equals(mediaSourceValueMicrophone)) { 
        // Specified 'audio/*' and requested microphone, so go ahead and launch the sound 
        // recorder. 
        startActivity(createSoundRecorderIntent()); 
        return; 
       } else { 
        // Specified just 'audio/*', capture=filesystem of an invalid capture parameter. 
        // In all these cases so go ahead and launch an intent for both the sound 
        // recorder and audio/* OPENABLE. 
        Intent chooser = createChooserIntent(createSoundRecorderIntent()); 
        chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(audioMimeType)); 
        startActivity(chooser); 
        return; 
       } 
      } 
      // No special handling based on the accept type was necessary, so trigger the default 
      // file upload chooser. 
      startActivity(createDefaultOpenableIntent()); 
     } 
     private void startActivity(Intent intent) { 
      try { 
       mController.getActivity().startActivityForResult(intent, Controller.FILE_SELECTED); 
      } catch (ActivityNotFoundException e) { 
       // No installed app was able to handle the intent that 
       // we sent, so fallback to the default file upload control. 
       try { 
        mCaughtActivityNotFoundException = true; 
        mController.getActivity().startActivityForResult(createDefaultOpenableIntent(), 
          Controller.FILE_SELECTED); 
       } catch (ActivityNotFoundException e2) { 
        // Nothing can return us a file, so file upload is effectively disabled. 
        Toast.makeText(mController.getActivity(), R.string.uploads_disabled, 
          Toast.LENGTH_LONG).show(); 
       } 
      } 
     } 
     private Intent createDefaultOpenableIntent() { 
      // Create and return a chooser with the default OPENABLE 
      // actions including the camera, camcorder and sound 
      // recorder where available. 
      Intent i = new Intent(Intent.ACTION_GET_CONTENT); 
      i.addCategory(Intent.CATEGORY_OPENABLE); 
      i.setType("*/*"); 
      Intent chooser = createChooserIntent(createCameraIntent(), createCamcorderIntent(), 
        createSoundRecorderIntent()); 
      chooser.putExtra(Intent.EXTRA_INTENT, i); 
      return chooser; 
     } 
     private Intent createChooserIntent(Intent... intents) { 
      Intent chooser = new Intent(Intent.ACTION_CHOOSER); 
      chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents); 
      chooser.putExtra(Intent.EXTRA_TITLE, 
        mController.getActivity().getResources() 
          .getString(R.string.choose_upload)); 
      return chooser; 
     } 
     private Intent createOpenableIntent(String type) { 
      Intent i = new Intent(Intent.ACTION_GET_CONTENT); 
      i.addCategory(Intent.CATEGORY_OPENABLE); 
      i.setType(type); 
      return i; 
     } 
     private Intent createCameraIntent() { 
      Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 
      File externalDataDir = Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_DCIM); 
      File cameraDataDir = new File(externalDataDir.getAbsolutePath() + 
        File.separator + "browser-photos"); 
      cameraDataDir.mkdirs(); 
      mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator + 
        System.currentTimeMillis() + ".jpg"; 
      cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(mCameraFilePath))); 
      return cameraIntent; 
     } 
     private Intent createCamcorderIntent() { 
      return new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 
     } 
     private Intent createSoundRecorderIntent() { 
      return new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); 
     } 


     public void onPageFinished(WebView view, String url) { 
      // Removes Progress Bar 
      findViewById(R.id.progressbar).setVisibility(View.GONE); 
      // Adds Cookies. Yummy! 
      CookieSyncManager.getInstance().sync(); 
     } 
} 
    @Override 
    public void onBackPressed() { 
     // Enables going back history 
     if (webView.copyBackForwardList().getCurrentIndex() > 0) { 
      webView.goBack(); 
     } 
     else { 
      // Your exit alert code, or alternatively line below to finish 
      // Finishes forum activity 
      super.onBackPressed(); 
     } 
    } 
} 

Вот данные LogCat:

12 - 18 03: 12: 53.095 28851 - 28851/com.technologx.technologx E/AndroidRuntime: FATAL EXCEPTION: 
    main 
Process: com.technologx.technologx, PID: 28851 

java.lang.RuntimeException: Unable to start activity ComponentInfo { 
    com.technologx.technologx/com.technologx.technologx.MainActivity 
}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.webkit.WebView.setWebViewClient(android.webkit.WebViewClient)' 
on a null object reference 
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java: 2379) 
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java: 2441) 
at android.app.ActivityThread.access$800(ActivityThread.java: 162) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java: 1349) 
at android.os.Handler.dispatchMessage(Handler.java: 102) 
at android.os.Looper.loop(Looper.java: 135) 
at android.app.ActivityThread.main(ActivityThread.java: 5431) 
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java: 372) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java: 914) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java: 707) 
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.webkit.WebView.setWebViewClient(android.webkit.WebViewClient)' 
on a null object reference 
at com.technologx.technologx.MainActivity.onCreate(MainActivity.java: 47) 
at android.app.Activity.performCreate(Activity.java: 6056) 
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java: 1106) 
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java: 2332) 
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java: 2441) 
at android.app.ActivityThread.access$800(ActivityThread.java: 162) 
at android.app.ActivityThread$H.handleMessage(ActivityThread.java: 1349) 
at android.os.Handler.dispatchMessage(Handler.java: 102) 
at android.os.Looper.loop(Looper.java: 135) 
at android.app.ActivityThread.main(ActivityThread.java: 5431) 
at java.lang.reflect.Method.invoke(Native Method) 
at java.lang.reflect.Method.invoke(Method.java: 372) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java: 914) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java: 707) 
+0

Похоже, что 'java.lang.NullPointerException' - это то, где вы должны искать. Может быть, вы вызываете 'webView.setWebViewClient (новый WebViewClient()) перед установкой переменной экземпляра' webView'? – halfer

+0

@halfer Я не уверен, что честно, как я сказал, что я ноб в кодировке Android. Мне лучше создавать веб-сайты или писать сценарии для iDevices. – Technologx

+0

Возможный дубликат [Что такое исключение NullPointerException и как его исправить?] (Http://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it) – halfer

ответ

1

Примечание Я не программист Java. В догадка, хотя, когда вы объявляете это, это будет пустая ссылка объект:

private WebView webView; 

Таким образом, в этих двух строках, вы пытаетесь сделать что-то с этим (нуль) объекта, а затем установить объект к примеру (который предположительно не равно нулю):

webView.setWebViewClient(new WebViewClient()); 

webView = (WebView) findViewById(R.id.activity_main_webview); 

Это согласуется с ошибкой:

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.webkit.WebView.setWebViewClient(android.webkit.WebViewClient)' on a null object reference

объект, который является пустым на самом деле не объект вообще, это просто пустой, и поэтому вы cann выполнить операцию над ним. Почему бы не попробовать поменять местами последние две строки и посмотреть, помогает ли это?

+0

Я исправил ошибку, добавив следующее: «WebView myWebView = (WebView) findViewById (R.id.activity_main_webview); // Закреплять ссылки и перенаправлять для открытия в WebView, а не в браузере webView.setWebViewClient (новый WebViewClient()); // Остановить локальные ссылки и перенаправлять от открытия в браузере вместо WebView webView.setWebViewClient (новый MyAppWebViewClient()); ниже «initWebView (webView); webView.loadUrl ("technologx.com"); // TODO вводит ваш url ', теперь индикатор выполнения работает неправильно. – Technologx

+1

Да, это тоже похоже на мое решение @Technologx. Большой! – halfer

+0

Это звучит как отдельная проблема @Technologx, которая была бы лучше всего сформулирована в новом вопросе.Помните, что читатели, как правило, вряд ли загружают ваш код и отлаживают его для себя (поскольку в тот день не хватает времени, чтобы помочь всем таким образом). Задайте новый вопрос, покажите свою новую ошибку, добавьте новый скриншот, если это необходимо, и если сможете, покажите, что вы сделали, чтобы решить проблему самостоятельно. Это очень хорошая практика! – halfer